18 커밋 b28614b5bb ... 3f3dad3984

작성자 SHA1 메시지 날짜
  Primroses 3f3dad3984 fix:v1.3版本的UI 3 년 전
  kidd3166 ee5347d9bc 1.3 ver 3 년 전
  kidd3166 8930b58c1f 1.3 ver 3 년 전
  kidd3166 d803c25446 1.3 ver 3 년 전
  kidd3166 f943e7b37c 1.3 ver 3 년 전
  kidd3166 820ec77654 1.3 ver 3 년 전
  kidd3166 aced10aaa6 1.3 ver 3 년 전
  kidd3166 75bad2b84d Merge branch 'd-dev' of http://svn.ouj.com:3000/ouj/flutter-sport into k-dev-20210122 3 년 전
  kidd3166 c4683b0663 1.3 ver 3 년 전
  kidd3166 5b7cc28e7d ios fix 3 년 전
  kidd3166 d334999279 Merge branch 'k-dev-20210122' of http://svn.ouj.com/ouj/flutter-sport into k-dev 3 년 전
  kidd3166 bd9d7fa3c2 1.3 ver 3 년 전
  kidd3166 b97552a778 Merge branches 'k-dev' and 'k-dev-20210122' of http://svn.ouj.com/ouj/flutter-sport into k-dev 3 년 전
  kidd3166 2a02c1b530 1.3 ver 3 년 전
  kidd3166 da5f31a48a ios fix 3 년 전
  kidd3166 19ea0bdcde Merge branches 'd-dev' and 'k-dev-20210122' of http://svn.ouj.com:3000/ouj/flutter-sport into k-dev-20210122 3 년 전
  kidd3166 e678bc6e21 Merge branch 'k-dev-20210122' of http://svn.ouj.com/ouj/flutter-sport into k-dev 3 년 전
  kidd3166 1b78e5806c Merge branches 'd-dev' and 'master' of http://svn.ouj.com/ouj/flutter-sport into k-dev 3 년 전
100개의 변경된 파일6417개의 추가작업 그리고 1369개의 파일을 삭제
  1. BIN
      fonts/DIN-BOLD.OTF
  2. 17 12
      ios/Runner.xcodeproj/project.pbxproj
  3. 4 4
      ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  4. 27 7
      ios/Runner/Info.plist
  5. BIN
      lib/assets/img/2.0x/image_refresh.png
  6. BIN
      lib/assets/img/2.0x/image_refresh_complete.png
  7. BIN
      lib/assets/img/image_refresh.png
  8. BIN
      lib/assets/img/image_refresh_complete.png
  9. 2 2
      lib/pages/game/game_info.dart
  10. 1 1
      lib/pages/game/rank_info.dart
  11. 389 389
      lib/pages/home/consume_page.dart
  12. 17 10
      lib/pages/home/duration_page.dart
  13. 31 34
      lib/pages/home/duration_setting_page.dart
  14. 8 7
      lib/pages/home/home_info_page.dart
  15. 2 1
      lib/pages/home/sport_history_all_page.dart
  16. 132 126
      lib/pages/home/sport_list_page.dart
  17. 29 27
      lib/pages/home/step_page.dart
  18. 1 9
      lib/pages/home/step_realtime_page.dart
  19. 9 7
      lib/pages/home/strength_page.dart
  20. 49 46
      lib/pages/home_page.dart
  21. 4 1
      lib/pages/my/achievement_detail_page.dart
  22. 343 188
      lib/pages/my/feedback_page.dart
  23. 14 1
      lib/pages/my/game_list_page.dart
  24. 1 1
      lib/pages/my/level_page.dart
  25. 1 8
      lib/pages/social/block_user_list_page.dart
  26. 74 24
      lib/pages/social/new_social_index_page.dart
  27. 1 11
      lib/pages/social/post_detail_page.dart
  28. 1 1
      lib/pages/social/post_widget.dart
  29. 27 22
      lib/pages/social/search_page.dart
  30. 1 11
      lib/pages/social/share_achievement.dart
  31. 61 76
      lib/pages/social/user_friend_add_page.dart
  32. 3 12
      lib/pages/social/user_friend_page.dart
  33. 0 14
      lib/pages/sport/calorie_page.dart
  34. 0 14
      lib/pages/sport/intensity_page.dart
  35. 0 89
      lib/pages/sport/target_page.dart
  36. 1 1
      lib/provider/bluetooth.dart
  37. 6 1
      lib/provider/message_model.dart
  38. 27 28
      lib/widgets/appbar.dart
  39. 4 4
      lib/widgets/chart.dart
  40. 493 0
      lib/widgets/circular_percent_indicator.dart
  41. 25 14
      lib/widgets/dialog/search_device.dart
  42. 30 21
      lib/widgets/dialog/share_popup.dart
  43. 8 6
      lib/widgets/loading.dart
  44. 60 107
      lib/widgets/menu_bar.dart
  45. 8 6
      lib/widgets/misc.dart
  46. 4 4
      lib/widgets/persistent_header.dart
  47. 1 2
      lib/widgets/refresh_header.dart
  48. 76 18
      lib/widgets/text_input.dart
  49. 3 0
      plugin/qrscanner/CHANGELOG.md
  50. 21 0
      plugin/qrscanner/LICENSE
  51. 43 0
      plugin/qrscanner/README.md
  52. 45 0
      plugin/qrscanner/android/build.gradle
  53. 4 0
      plugin/qrscanner/android/gradle.properties
  54. 5 0
      plugin/qrscanner/android/gradle/wrapper/gradle-wrapper.properties
  55. 2 0
      plugin/qrscanner/android/local.properties
  56. 1 0
      plugin/qrscanner/android/settings.gradle
  57. 16 0
      plugin/qrscanner/android/src/main/AndroidManifest.xml
  58. 77 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/AmbientLightManager.java
  59. 112 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/BeepManager.java
  60. 426 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/CaptureActivity.java
  61. 163 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/CaptureActivityHandler.java
  62. 118 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/Contents.java
  63. 103 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/DecodeFormatManager.java
  64. 107 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/DecodeHandler.java
  65. 231 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/DecodeHintManager.java
  66. 90 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/DecodeThread.java
  67. 49 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/FinishListener.java
  68. 122 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/InactivityTimer.java
  69. 201 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/Intents.java
  70. 156 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/ViewfinderView.java
  71. 126 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/AutoFocusManager.java
  72. 237 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/CameraConfigurationManager.java
  73. 433 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/CameraConfigurationUtils.java
  74. 352 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/CameraManager.java
  75. 57 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/PreviewCallback.java
  76. 27 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/open/CameraFacing.java
  77. 56 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/open/OpenCamera.java
  78. 85 0
      plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/open/OpenCameraInterface.java
  79. 111 0
      plugin/qrscanner/android/src/main/kotlin/com/alva/flutter/plugin/qrscanner/QrScannerPlugin.kt
  80. BIN
      plugin/qrscanner/android/src/main/res/drawable-xhdpi/btn_arrow_back.png
  81. BIN
      plugin/qrscanner/android/src/main/res/drawable-xhdpi/scan_finder_net.png
  82. 76 0
      plugin/qrscanner/android/src/main/res/layout/capture.xml
  83. BIN
      plugin/qrscanner/android/src/main/res/raw/beep.ogg
  84. 5 0
      plugin/qrscanner/android/src/main/res/values/colors.xml
  85. 9 0
      plugin/qrscanner/android/src/main/res/values/strings.xml
  86. 7 0
      plugin/qrscanner/android/src/main/res/values/styles.xml
  87. 35 0
      plugin/qrscanner/ios/Assets/FlutterQrScanner.xib
  88. BIN
      plugin/qrscanner/ios/Assets/arrow_dark_left.png
  89. BIN
      plugin/qrscanner/ios/Assets/arrow_dark_left@2x.png
  90. BIN
      plugin/qrscanner/ios/Assets/arrow_left.png
  91. BIN
      plugin/qrscanner/ios/Assets/arrow_left@2x.png
  92. BIN
      plugin/qrscanner/ios/Assets/scannet.png
  93. 4 0
      plugin/qrscanner/ios/Classes/QrScannerPlugin.h
  94. 15 0
      plugin/qrscanner/ios/Classes/QrScannerPlugin.m
  95. 479 0
      plugin/qrscanner/ios/Classes/ScannerViewController.swift
  96. 72 0
      plugin/qrscanner/ios/Classes/SwiftQrScannerPlugin.swift
  97. 24 0
      plugin/qrscanner/ios/qrscanner.podspec
  98. 49 0
      plugin/qrscanner/lib/flutter_plugin_qr_scanner.dart
  99. 65 0
      plugin/qrscanner/pubspec.yaml
  100. 6 2
      pubspec.yaml

BIN
fonts/DIN-BOLD.OTF


+ 17 - 12
ios/Runner.xcodeproj/project.pbxproj

@@ -39,12 +39,13 @@
 		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
 		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+		9358178825E34A4400B465DE /* flutter_plugin_qr_scanner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = flutter_plugin_qr_scanner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
 		93B3E2E72509CE3F00480584 /* Hiyd ShoesDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Hiyd ShoesDebug.entitlements"; sourceTree = "<group>"; };
 		93D0E6B724E1346700E152ED /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
 		93D14A3124D901D400C3395A /* FlutterBridging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlutterBridging.swift; 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 /* Hiyd Shoes.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Hiyd Shoes.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		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>"; };
@@ -78,6 +79,7 @@
 		6A3BBDB0D04FFB47B62E2897 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				9358178825E34A4400B465DE /* flutter_plugin_qr_scanner.framework */,
 				CA0E01D19708244D8D4F492B /* Pods_Runner.framework */,
 			);
 			name = Frameworks;
@@ -109,7 +111,7 @@
 		97C146EF1CF9000F007C117D /* Products */ = {
 			isa = PBXGroup;
 			children = (
-				97C146EE1CF9000F007C117D /* Hiyd Shoes.app */,
+				97C146EE1CF9000F007C117D /* Runner.app */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -161,7 +163,7 @@
 			);
 			name = Runner;
 			productName = Runner;
-			productReference = 97C146EE1CF9000F007C117D /* Hiyd Shoes.app */;
+			productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
 			productType = "com.apple.product-type.application";
 		};
 /* End PBXNativeTarget section */
@@ -346,6 +348,7 @@
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				COPY_PHASE_STRIP = NO;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_BITCODE = YES;
 				ENABLE_NS_ASSERTIONS = NO;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu99;
@@ -356,7 +359,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = iphoneos;
 				SUPPORTED_PLATFORMS = iphoneos;
@@ -372,7 +375,7 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
-				CURRENT_PROJECT_VERSION = 6;
+				CURRENT_PROJECT_VERSION = 8;
 				DEVELOPMENT_TEAM = 79QQ6HAK8M;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
@@ -390,7 +393,7 @@
 					"$(PROJECT_DIR)/Flutter",
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = xie.hiyd.com;
-				PRODUCT_NAME = "Hiyd Shoes";
+				PRODUCT_NAME = Runner;
 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = 1;
@@ -430,6 +433,7 @@
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				COPY_PHASE_STRIP = NO;
 				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_BITCODE = YES;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
 				ENABLE_TESTABILITY = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu99;
@@ -446,7 +450,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
 				MTL_ENABLE_DEBUG_INFO = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = iphoneos;
@@ -486,6 +490,7 @@
 				"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
 				COPY_PHASE_STRIP = NO;
 				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_BITCODE = YES;
 				ENABLE_NS_ASSERTIONS = NO;
 				ENABLE_STRICT_OBJC_MSGSEND = YES;
 				GCC_C_LANGUAGE_STANDARD = gnu99;
@@ -496,7 +501,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 12.0;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				SDKROOT = iphoneos;
 				SUPPORTED_PLATFORMS = iphoneos;
@@ -514,7 +519,7 @@
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
 				CODE_SIGN_ENTITLEMENTS = "Runner/Hiyd ShoesDebug.entitlements";
-				CURRENT_PROJECT_VERSION = 6;
+				CURRENT_PROJECT_VERSION = 8;
 				DEVELOPMENT_TEAM = 79QQ6HAK8M;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
@@ -532,7 +537,7 @@
 					"$(PROJECT_DIR)/Flutter",
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = xie.hiyd.com;
-				PRODUCT_NAME = "Hiyd Shoes";
+				PRODUCT_NAME = Runner;
 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 				SWIFT_VERSION = 5.0;
@@ -547,7 +552,7 @@
 			buildSettings = {
 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
 				CLANG_ENABLE_MODULES = YES;
-				CURRENT_PROJECT_VERSION = 6;
+				CURRENT_PROJECT_VERSION = 8;
 				DEVELOPMENT_TEAM = 79QQ6HAK8M;
 				ENABLE_BITCODE = NO;
 				FRAMEWORK_SEARCH_PATHS = (
@@ -565,7 +570,7 @@
 					"$(PROJECT_DIR)/Flutter",
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = xie.hiyd.com;
-				PRODUCT_NAME = "Hiyd Shoes";
+				PRODUCT_NAME = Runner;
 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = 1;

+ 4 - 4
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

@@ -15,7 +15,7 @@
             <BuildableReference
                BuildableIdentifier = "primary"
                BlueprintIdentifier = "97C146ED1CF9000F007C117D"
-               BuildableName = "Hiyd Shoes.app"
+               BuildableName = "Runner.app"
                BlueprintName = "Runner"
                ReferencedContainer = "container:Runner.xcodeproj">
             </BuildableReference>
@@ -31,7 +31,7 @@
          <BuildableReference
             BuildableIdentifier = "primary"
             BlueprintIdentifier = "97C146ED1CF9000F007C117D"
-            BuildableName = "Hiyd Shoes.app"
+            BuildableName = "Runner.app"
             BlueprintName = "Runner"
             ReferencedContainer = "container:Runner.xcodeproj">
          </BuildableReference>
@@ -54,7 +54,7 @@
          <BuildableReference
             BuildableIdentifier = "primary"
             BlueprintIdentifier = "97C146ED1CF9000F007C117D"
-            BuildableName = "Hiyd Shoes.app"
+            BuildableName = "Runner.app"
             BlueprintName = "Runner"
             ReferencedContainer = "container:Runner.xcodeproj">
          </BuildableReference>
@@ -71,7 +71,7 @@
          <BuildableReference
             BuildableIdentifier = "primary"
             BlueprintIdentifier = "97C146ED1CF9000F007C117D"
-            BuildableName = "Hiyd Shoes.app"
+            BuildableName = "Runner.app"
             BlueprintName = "Runner"
             ReferencedContainer = "container:Runner.xcodeproj">
          </BuildableReference>

+ 27 - 7
ios/Runner/Info.plist

@@ -2,16 +2,16 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-	<key>CFBundleLocalizations</key>
-	<array>
-		<string>zh_CN</string>
-	</array>
 	<key>CFBundleExecutable</key>
 	<string>$(EXECUTABLE_NAME)</string>
 	<key>CFBundleIdentifier</key>
 	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
 	<key>CFBundleInfoDictionaryVersion</key>
 	<string>6.0</string>
+	<key>CFBundleLocalizations</key>
+	<array>
+		<string>zh_CN</string>
+	</array>
 	<key>CFBundleName</key>
 	<string>sport</string>
 	<key>CFBundlePackageType</key>
@@ -42,6 +42,26 @@
 				<string>tencent1110701531</string>
 			</array>
 		</dict>
+		<dict>
+			<key>CFBundleTypeRole</key>
+			<string>Editor</string>
+			<key>CFBundleURLName</key>
+			<string>SDKRunGane</string>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>com.SDKRunGane.www</string>
+			</array>
+		</dict>
+		<dict>
+			<key>CFBundleTypeRole</key>
+			<string>Editor</string>
+			<key>CFBundleURLName</key>
+			<string>SDKDanceGane</string>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>com.SDKDanceGane.www</string>
+			</array>
+		</dict>
 	</array>
 	<key>CFBundleVersion</key>
 	<string>$(CURRENT_PROJECT_VERSION)</string>
@@ -73,7 +93,7 @@
 	<key>NSCalendarsUsageDescription</key>
 	<string>是否允许此App使用日历?</string>
 	<key>NSCameraUsageDescription</key>
-	<string>Multi Image Picker</string>
+	<string>是否允许访问相机?</string>
 	<key>NSContactsUsageDescription</key>
 	<string>是否允许此App读取通讯录信息?</string>
 	<key>NSLocationAlwaysUsageDescription</key>
@@ -84,10 +104,10 @@
 	<string>是否允许此App使用你的麦克风?</string>
 	<key>NSMotionUsageDescription</key>
 	<string>是否允许访问运动与健身?</string>
-	<key>NSPhotoLibraryUsageDescription</key>
-	<string>是否允许访问照片?</string>
 	<key>NSPhotoLibraryAddUsageDescription</key>
 	<string>是否允许添加照片?</string>
+	<key>NSPhotoLibraryUsageDescription</key>
+	<string>是否允许访问照片?</string>
 	<key>NSRemindersUsageDescription</key>
 	<string>是否允许此App访问提醒事项?</string>
 	<key>NSSiriUsageDescription</key>

BIN
lib/assets/img/2.0x/image_refresh.png


BIN
lib/assets/img/2.0x/image_refresh_complete.png


BIN
lib/assets/img/image_refresh.png


BIN
lib/assets/img/image_refresh_complete.png


+ 2 - 2
lib/pages/game/game_info.dart

@@ -292,8 +292,8 @@ class GameItem extends StatelessWidget {
                   Text(
                     "$name",
                     style: bold
-                        ? Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 16.0, fontWeight: FontWeight.w600)
-                        : Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 16.0),
+                        ? Theme.of(context).textTheme.headline1.copyWith(fontSize: 16.0, fontWeight: FontWeight.w600)
+                        : Theme.of(context).textTheme.headline1.copyWith(fontSize: 16.0,fontWeight: FontWeight.bold),
                   ),
                   SizedBox(
                     height: 3,

+ 1 - 1
lib/pages/game/rank_info.dart

@@ -72,7 +72,7 @@ class _RankInfoState extends State<RankInfo> with InjectApi, AutomaticKeepAliveC
             ),
             // label
             Padding(
-              padding: const EdgeInsets.only(top: 5.0),
+              padding: const EdgeInsets.only(top: 0.0),
               child: buildLabelWidget(context, "运动评分榜"),
             ),
             // 游戏评分榜

+ 389 - 389
lib/pages/home/consume_page.dart

@@ -10,6 +10,7 @@ import 'package:sport/router/navigator_util.dart';
 import 'package:sport/services/api/inject_api.dart';
 import 'package:sport/services/api/resp.dart';
 import 'package:sport/utils/date.dart';
+import 'package:sport/utils/toast.dart';
 import 'package:sport/widgets/appbar.dart';
 import 'package:sport/widgets/chart.dart';
 import 'package:sport/widgets/decoration.dart';
@@ -54,128 +55,128 @@ class _PageState extends State<ConsumePage> with InjectApi {
 
   @override
   Widget build(BuildContext context) {
-    final double tabHeader = 100.0;
+    final double tabHeader = 80.0;
     final double statusBarHeight = MediaQuery.of(context).padding.top;
-    final double pinnedHeaderHeight = tabHeader + statusBarHeight;
+    final double pinnedHeaderHeight = tabHeader;
     final double headerHeight = 240.0;
     return Scaffold(
       backgroundColor: Colors.white,
-      body: Stack(
-        children: <Widget>[
-          extended.NestedScrollView(
-            controller: _scrollController,
-            pinnedHeaderSliverHeightBuilder: () {
-              return pinnedHeaderHeight;
-            },
-            innerScrollPositionKeyBuilder: () {
-              PageController controller = _pageController;
-              String index = 'Tab${controller.page}';
-              return Key(index);
-            },
-            headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
-              return <Widget>[
-                SliverToBoxAdapter(
-                  child: Container(
-                    width: 240.0,
-                    height: headerHeight,
-                    child: Align(
-                      alignment: Alignment.bottomCenter,
-                      child: CustomPaint(
-                        painter: _Bg(),
-                        child: Container(
-                          width: 180.0,
-                          height: 180.0,
-                          child: Center(
-                            child: Column(
-                              children: <Widget>[
-                                Text("消耗卡路里", style: Theme.of(context).textTheme.subtitle1),
-                                SizedBox(
-                                  height: 26.0,
-                                ),
-                                Row(
-                                  children: <Widget>[
-                                    ValueListenableBuilder(
-                                      builder: (BuildContext context, value, Widget child) => FutureBuilder(
-                                        future: createFutureType(0, _valueNotifierNow.value),
-                                        builder: (BuildContext context, AsyncSnapshot<SportDetail> snapshot) => Text(
-                                          "${snapshot?.data?.recordsTodaySum?.consume ?? 0}",
-                                          style: Theme.of(context).textTheme.headline1.copyWith(fontSize: 40.0),
-                                          strutStyle: fixedLine,
-                                        ),
-                                      ),
-                                      valueListenable: _valueNotifierNow,
-                                    ),
-                                    Text(" 卡", style: Theme.of(context).textTheme.subtitle2),
-                                  ],
-                                  mainAxisSize: MainAxisSize.min,
-                                  crossAxisAlignment: CrossAxisAlignment.end,
-                                ),
-                                SizedBox(
-                                  height: 8,
-                                ),
-                                GestureDetector(
-                                  onTap: () async {
-                                    var result = await showDatePicker(
-                                      context: context,
-                                      initialDate: _valueNotifierNow.value,
-                                      lastDate: DateTime.now(),
-                                      firstDate: DateTime(2020),
-                                    );
-                                    if (result != null) {
-                                      var diff = DateTime.now().difference(result);
-                                      _valueNotifierDate.value = result;
-                                      _valueNotifierNow.value = result;
-                                      int type = toType();
-                                      // if (type == 0) {
-                                      //   _pageController.jumpToPage(diff.inDays);
-                                      // } else {
-                                      //   _pageController = PageController(initialPage: diff.inDays);
-                                      // }
-
-                                      _tab.value = TABS.first;
-                                      _pageController.jumpToPage(diff.inDays);
-                                      print("$type -- ${diff.inDays}");
-                                    }
-                                  },
-                                  child: Row(
+      body: SafeArea(
+        child: Stack(
+          children: <Widget>[
+            extended.NestedScrollView(
+              controller: _scrollController,
+              pinnedHeaderSliverHeightBuilder: () {
+                return pinnedHeaderHeight;
+              },
+              innerScrollPositionKeyBuilder: () {
+                PageController controller = _pageController;
+                String index = 'Tab${controller.page}';
+                return Key(index);
+              },
+              headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) {
+                return <Widget>[
+                  SliverToBoxAdapter(
+                    child: Container(
+                      width: 240.0,
+                      height: headerHeight,
+                      child: Align(
+                        alignment: Alignment.center,
+                        child: CustomPaint(
+                          painter: _Bg(),
+                          child: Container(
+                            width: 180.0,
+                            height: 180.0,
+                            child: Center(
+                              child: Column(
+                                children: <Widget>[
+                                  Text("消耗卡路里", style: Theme.of(context).textTheme.subtitle1),
+                                  SizedBox(
+                                    height: 26.0,
+                                  ),
+                                  Row(
                                     children: <Widget>[
                                       ValueListenableBuilder(
+                                        builder: (BuildContext context, value, Widget child) => FutureBuilder(
+                                          future: createFutureType(0, _valueNotifierNow.value),
+                                          builder: (BuildContext context, AsyncSnapshot<SportDetail> snapshot) => Text(
+                                            "${snapshot?.data?.recordsTodaySum?.consume ?? 0}",
+                                            style: Theme.of(context).textTheme.headline1.copyWith(fontSize: 40.0),
+                                            strutStyle: fixedLine,
+                                          ),
+                                        ),
                                         valueListenable: _valueNotifierNow,
-                                        builder: (BuildContext context, DateTime value, Widget child) =>
-                                            Text("${value.month}.${value.day}", style: Theme.of(context).textTheme.subtitle1),
-                                      ),
-                                      SizedBox(
-                                        width: 12.0,
                                       ),
-                                      Image.asset("lib/assets/img/setgoals_icon_date.png"),
+                                      Text(" 卡", style: Theme.of(context).textTheme.subtitle2),
                                     ],
                                     mainAxisSize: MainAxisSize.min,
+                                    crossAxisAlignment: CrossAxisAlignment.end,
                                   ),
-                                  behavior: HitTestBehavior.opaque,
-                                )
-                              ],
-                              mainAxisSize: MainAxisSize.min,
+                                  SizedBox(
+                                    height: 8,
+                                  ),
+                                  GestureDetector(
+                                    onTap: () async {
+                                      var result = await showDatePicker(
+                                        context: context,
+                                        initialDate: _valueNotifierNow.value,
+                                        lastDate: DateTime.now(),
+                                        firstDate: DateTime(2020),
+                                      );
+                                      if (result != null) {
+                                        var diff = DateTime.now().difference(result);
+                                        _valueNotifierDate.value = result;
+                                        _valueNotifierNow.value = result;
+                                        int type = toType();
+                                        // if (type == 0) {
+                                        //   _pageController.jumpToPage(diff.inDays);
+                                        // } else {
+                                        //   _pageController = PageController(initialPage: diff.inDays);
+                                        // }
+
+                                        _tab.value = TABS.first;
+                                        _pageController.jumpToPage(diff.inDays);
+                                        print("$type -- ${diff.inDays}");
+                                      }
+                                    },
+                                    child: Row(
+                                      children: <Widget>[
+                                        ValueListenableBuilder(
+                                          valueListenable: _valueNotifierNow,
+                                          builder: (BuildContext context, DateTime value, Widget child) =>
+                                              Text("${value.month}.${value.day}", style: Theme.of(context).textTheme.subtitle1),
+                                        ),
+                                        SizedBox(
+                                          width: 12.0,
+                                        ),
+                                        Image.asset("lib/assets/img/setgoals_icon_date.png"),
+                                      ],
+                                      mainAxisSize: MainAxisSize.min,
+                                    ),
+                                    behavior: HitTestBehavior.opaque,
+                                  )
+                                ],
+                                mainAxisSize: MainAxisSize.min,
+                              ),
                             ),
                           ),
                         ),
                       ),
                     ),
                   ),
-                ),
-                SliverPersistentHeader(
-                  pinned: true,
-                  delegate: PersistentHeader(
-                    min: pinnedHeaderHeight,
-                    max: pinnedHeaderHeight,
-                    child: Container(
-                      color: Colors.white,
-                      child: ValueListenableBuilder(
-                        valueListenable: _tab,
-                        builder: (BuildContext context, String value, Widget child) {
-                          return Column(
-                            children: <Widget>[
-                              SafeArea(
-                                child: Padding(
+                  SliverPersistentHeader(
+                    pinned: true,
+                    delegate: PersistentHeader(
+                      min: pinnedHeaderHeight,
+                      max: pinnedHeaderHeight,
+                      child: Container(
+                        color: Colors.white,
+                        child: ValueListenableBuilder(
+                          valueListenable: _tab,
+                          builder: (BuildContext context, String value, Widget child) {
+                            return Column(
+                              children: <Widget>[
+                                Padding(
                                   padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 5.0),
                                   child: Row(
                                     mainAxisAlignment: MainAxisAlignment.center,
@@ -212,313 +213,312 @@ class _PageState extends State<ConsumePage> with InjectApi {
                                         .toList(),
                                   ),
                                 ),
-                              ),
-                              Center(
-                                child: ValueListenableBuilder<DateTime>(
-                                    valueListenable: _valueNotifierDate,
-                                    builder: (_, time, ___) {
-                                      int type = toType();
-                                      String text = "";
-                                      if (type == 0) {
-                                        text = "${time.year}.${'${time.month}'.padLeft(2, '0')}.${'${time.day}'.padLeft(2, '0')}  6:00 - 24:00 ";
-                                      } else if (type == 1) {
-                                        DateTime start = DateTime(time.year, time.month, time.day - time.weekday + 1);
-                                        DateTime end = DateTime(time.year, time.month, time.day + 6 - time.weekday + 1);
-                                        print("$time ${time.weekday} ==  $start $end");
-                                        text =
-                                            "${start.year}.${'${start.month}'.padLeft(2, '0')}.${'${start.day}'.padLeft(2, '0')}  ~  ${end.year}.${'${end.month}'.padLeft(2, '0')}.${'${end.day}'.padLeft(2, '0')}";
-                                      } else if (type == 2) {
-                                        text = ("${time.year}年${'${time.month}'.padLeft(2, '0')}月");
-                                      } else if (type == 3) {
-                                        text = ("${time.year}年");
-                                      }
-                                      return Row(
-                                        mainAxisSize: MainAxisSize.min,
-                                        children: <Widget>[
-                                          GestureDetector(
-                                            behavior: HitTestBehavior.opaque,
-                                            onTap: () {
-                                              _pageController?.nextPage(duration: Duration(milliseconds: 500), curve: Curves.linear);
-                                            },
-                                            child: Padding(
-                                              padding: const EdgeInsets.all(18.0),
-                                              child: arrowLeft(),
+                                Center(
+                                  child: ValueListenableBuilder<DateTime>(
+                                      valueListenable: _valueNotifierDate,
+                                      builder: (_, time, ___) {
+                                        int type = toType();
+                                        String text = "";
+                                        if (type == 0) {
+                                          text = "${time.year}.${'${time.month}'.padLeft(2, '0')}.${'${time.day}'.padLeft(2, '0')}  6:00 - 24:00 ";
+                                        } else if (type == 1) {
+                                          DateTime start = DateTime(time.year, time.month, time.day - time.weekday + 1);
+                                          DateTime end = DateTime(time.year, time.month, time.day + 6 - time.weekday + 1);
+                                          print("$time ${time.weekday} ==  $start $end");
+                                          text =
+                                              "${start.year}.${'${start.month}'.padLeft(2, '0')}.${'${start.day}'.padLeft(2, '0')}  ~  ${end.year}.${'${end.month}'.padLeft(2, '0')}.${'${end.day}'.padLeft(2, '0')}";
+                                        } else if (type == 2) {
+                                          text = ("${time.year}年${'${time.month}'.padLeft(2, '0')}月");
+                                        } else if (type == 3) {
+                                          text = ("${time.year}年");
+                                        }
+                                        return Row(
+                                          mainAxisSize: MainAxisSize.min,
+                                          children: <Widget>[
+                                            GestureDetector(
+                                              behavior: HitTestBehavior.opaque,
+                                              onTap: () {
+                                                _pageController?.nextPage(duration: Duration(milliseconds: 500), curve: Curves.linear);
+                                              },
+                                              child: Padding(
+                                                padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 10.0),
+                                                child: arrowLeft(),
+                                              ),
                                             ),
-                                          ),
-                                          Text(
-                                            text,
-                                            style: Theme.of(context).textTheme.bodyText2.copyWith(color: Color(0xff333333)),
-                                            strutStyle: fixedLine,
-                                          ),
-                                          GestureDetector(
-                                            behavior: HitTestBehavior.opaque,
-                                            onTap: () {
-                                              if (_pageController?.page == 0.0) {
-                                                return;
-                                              }
-                                              _pageController?.previousPage(duration: Duration(milliseconds: 500), curve: Curves.linear);
-                                            },
-                                            child: Padding(
-                                              padding: const EdgeInsets.all(18.0),
-                                              child: arrowRight(),
+                                            Text(
+                                              text,
+                                              style: Theme.of(context).textTheme.bodyText2.copyWith(color: Color(0xff333333)),
+                                              strutStyle: fixedLine,
                                             ),
-                                          ),
-                                        ],
-                                      );
-                                    }),
-                              ),
-                            ],
-                          );
-                        },
+                                            GestureDetector(
+                                              behavior: HitTestBehavior.opaque,
+                                              onTap: () {
+                                                if (_pageController?.page == 0.0) {
+                                                  ToastUtil.show("没有数据了");
+                                                  return;
+                                                }
+                                                _pageController?.previousPage(duration: Duration(milliseconds: 500), curve: Curves.linear);
+                                              },
+                                              child: Padding(
+                                                padding: const EdgeInsets.symmetric(horizontal: 18.0, vertical: 10.0),
+                                                child: arrowRight(),
+                                              ),
+                                            ),
+                                          ],
+                                        );
+                                      }),
+                                ),
+                              ],
+                            );
+                          },
+                        ),
                       ),
                     ),
                   ),
-                ),
-              ];
-            },
-            body: ValueListenableBuilder(
-              valueListenable: _tab,
-              builder: (BuildContext context, String value, Widget child) => PageView.builder(
-                reverse: true,
-                itemCount: 10240,
-                controller: _pageController,
-                onPageChanged: (page) {
-                  rollDate(-page);
-                },
-                itemBuilder: (context, index) {
-                  int type = toType();
-                  DateTime time = offsetDate(type, -index);
-                  print("$index $type --2222 ${time}");
-                  return extended.NestedScrollViewInnerScrollPositionKeyWidget(
-                      Key('Tab$index'),
-                      FutureBuilder<SportDetail>(
-                          future: createFuture(time),
-                          builder: (BuildContext context, AsyncSnapshot<SportDetail> snapshot) {
-                            var _value = snapshot?.data;
-                            var _items = _createItems(type, _value?.recordsTodaySum);
-                            return snapshot.connectionState != ConnectionState.done
-                                ? RequestLoadingWidget()
-                                : Padding(
-                                    padding: const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 0),
-                                    child: SingleChildScrollView(
-                                      child: Column(
-                                        children: <Widget>[
-                                          SizedBox(
-                                            height: 10.0,
-                                          ),
-                                          Padding(
-                                            padding: const EdgeInsets.only(right: 12.0),
-                                            child: CustomPaint(
-                                              painter: Chart(
-                                                  type: TABS.indexOf(_tab.value),
-                                                  records: _value?.recordsToday
-                                                          ?.map((e) => ChartItem(type == 3 ? "${e.month}" : e.createdAt, e.consume))
-                                                          ?.toList() ??
-                                                      [],
-                                                  dateTime: time,
-                                                  drawMax: true,
-                                                  unit: "kal")
-                                                ..initData(maxValue: 3500.0 * (type + 1), valueSplit: 500),
-                                              child: Container(
-                                                height: 200,
+                ];
+              },
+              body: ValueListenableBuilder(
+                valueListenable: _tab,
+                builder: (BuildContext context, String value, Widget child) => PageView.builder(
+                  reverse: true,
+                  itemCount: 10240,
+                  controller: _pageController,
+                  onPageChanged: (page) {
+                    rollDate(-page);
+                  },
+                  itemBuilder: (context, index) {
+                    int type = toType();
+                    DateTime time = offsetDate(type, -index);
+                    print("$index $type --2222 ${time}");
+                    return extended.NestedScrollViewInnerScrollPositionKeyWidget(
+                        Key('Tab$index'),
+                        FutureBuilder<SportDetail>(
+                            future: createFuture(time),
+                            builder: (BuildContext context, AsyncSnapshot<SportDetail> snapshot) {
+                              var _value = snapshot?.data;
+                              var _items = _createItems(type, _value?.recordsTodaySum);
+                              return snapshot.connectionState != ConnectionState.done
+                                  ? RequestLoadingWidget()
+                                  : Padding(
+                                      padding: const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 0),
+                                      child: SingleChildScrollView(
+                                        child: Column(
+                                          children: <Widget>[
+                                            SizedBox(
+                                              height: 10.0,
+                                            ),
+                                            Padding(
+                                              padding: const EdgeInsets.only(right: 12.0),
+                                              child: CustomPaint(
+                                                painter: Chart(
+                                                    type: TABS.indexOf(_tab.value),
+                                                    records: _value?.recordsToday
+                                                            ?.map((e) => ChartItem(type == 3 ? "${e.month}" : e.createdAt, e.consume))
+                                                            ?.toList() ??
+                                                        [],
+                                                    dateTime: time,
+                                                    drawMax: true,
+                                                    unit: "kal")
+                                                  ..initData(maxValue: 3500.0 * (type + 1), valueSplit: 500),
+                                                child: Container(
+                                                  height: 200,
+                                                ),
                                               ),
                                             ),
-                                          ),
-                                          const SizedBox(
-                                            width: 12.0,
-                                          ),
-                                          Padding(
-                                            padding: const EdgeInsets.fromLTRB(8, 8, 8, 4),
-                                            child: StaggeredGridView.extent(
-                                                maxCrossAxisExtent: (MediaQuery.of(context).size.width - 32.0) / 2,
-                                                padding: EdgeInsets.zero,
-                                                shrinkWrap: true,
-                                                physics: NeverScrollableScrollPhysics(),
-                                                mainAxisSpacing: 12.0,
-                                                crossAxisSpacing: 12.0,
-                                                children: _items
-                                                    .map((e) => Container(
-                                                          decoration: card(),
-                                                          padding: const EdgeInsets.fromLTRB(20.0, 20.0, 0, 20.0),
-                                                          child: Row(
+                                            const SizedBox(
+                                              width: 12.0,
+                                            ),
+                                            Padding(
+                                              padding: const EdgeInsets.fromLTRB(8, 8, 8, 4),
+                                              child: StaggeredGridView.extent(
+                                                  maxCrossAxisExtent: (MediaQuery.of(context).size.width - 32.0) / 2,
+                                                  padding: EdgeInsets.zero,
+                                                  shrinkWrap: true,
+                                                  physics: NeverScrollableScrollPhysics(),
+                                                  mainAxisSpacing: 12.0,
+                                                  crossAxisSpacing: 12.0,
+                                                  children: _items
+                                                      .map((e) => Container(
+                                                            decoration: card(),
+                                                            padding: const EdgeInsets.fromLTRB(20.0, 20.0, 0, 20.0),
+                                                            child: Row(
+                                                              children: <Widget>[
+                                                                Image.asset(
+                                                                  e.icon,
+                                                                  width: 36.0,
+                                                                ),
+                                                                const SizedBox(
+                                                                  width: 10.0,
+                                                                ),
+                                                                Column(
+                                                                  crossAxisAlignment: CrossAxisAlignment.start,
+                                                                  children: <Widget>[
+                                                                    Row(
+                                                                      crossAxisAlignment: CrossAxisAlignment.end,
+                                                                      children: <Widget>[
+                                                                        Text(
+                                                                          e.title,
+                                                                          style: e.unit != "" ? Theme.of(context).textTheme.headline1.copyWith(fontSize: 20.0):Theme.of(context).textTheme.headline1.copyWith(fontSize: 16.0),
+                                                                          strutStyle: fixedLine,
+                                                                        ),
+                                                                        Text(" ${e.unit}", style: Theme.of(context).textTheme.subtitle2),
+                                                                      ],
+                                                                    ),
+                                                                    const SizedBox(
+                                                                      width: 4.0,
+                                                                    ),
+                                                                    Text(e.subtitle, style: Theme.of(context).textTheme.bodyText1)
+                                                                  ],
+                                                                )
+                                                              ],
+                                                            ),
+                                                          ))
+                                                      .toList(),
+                                                  staggeredTiles: _items.map((e) => StaggeredTile.fit(1)).toList()),
+                                            ),
+                                            if (type != 0 && _value?.recordsTodayAvg != null)
+                                              Padding(
+                                                padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 20.0),
+                                                child: Column(
+                                                  crossAxisAlignment: CrossAxisAlignment.start,
+                                                  children: <Widget>[
+                                                    Text(
+                                                      "日均数据",
+                                                      style: Theme.of(context).textTheme.headline1.copyWith(fontSize: 16.0),
+                                                    ),
+                                                    SizedBox(
+                                                      height: 16.0,
+                                                    ),
+                                                    Container(
+                                                      padding: const EdgeInsets.fromLTRB(14.0, 21.0, 14.0, 21.0),
+                                                      decoration: card(),
+                                                      child: Column(
+                                                        children: <Widget>[
+                                                          Row(
                                                             children: <Widget>[
                                                               Image.asset(
-                                                                e.icon,
-                                                                width: 36.0,
+                                                                "lib/assets/img/day_icon_duration.png",
+                                                                width: 19.0,
                                                               ),
                                                               const SizedBox(
-                                                                width: 10.0,
+                                                                width: 8.0,
+                                                              ),
+                                                              Expanded(
+                                                                child: Text(
+                                                                  "日均时长",
+                                                                  style: Theme.of(context).textTheme.subtitle1,
+                                                                ),
+                                                              ),
+                                                              SizedBox(
+                                                                width: 60.0,
+                                                                child: Text(
+                                                                  "${_value?.recordsTodayAvg?.durationMinute ?? 0}分钟",
+                                                                  style: Theme.of(context).textTheme.subtitle1,
+                                                                ),
                                                               ),
-                                                              Column(
-                                                                crossAxisAlignment: CrossAxisAlignment.start,
-                                                                children: <Widget>[
-                                                                  Row(
-                                                                    crossAxisAlignment: CrossAxisAlignment.end,
-                                                                    children: <Widget>[
-                                                                      Text(
-                                                                        e.title,
-                                                                        style: e.unit != "" ? Theme.of(context).textTheme.headline1.copyWith(fontSize: 20.0):Theme.of(context).textTheme.headline1.copyWith(fontSize: 16.0),
-                                                                        strutStyle: fixedLine,
-                                                                      ),
-                                                                      Text(" ${e.unit}", style: Theme.of(context).textTheme.subtitle2),
-                                                                    ],
-                                                                  ),
-                                                                  const SizedBox(
-                                                                    width: 4.0,
-                                                                  ),
-                                                                  Text(e.subtitle, style: Theme.of(context).textTheme.bodyText1)
-                                                                ],
-                                                              )
                                                             ],
                                                           ),
-                                                        ))
-                                                    .toList(),
-                                                staggeredTiles: _items.map((e) => StaggeredTile.fit(1)).toList()),
-                                          ),
-                                          if (type != 0 && _value?.recordsTodayAvg != null)
-                                            Padding(
-                                              padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 20.0),
-                                              child: Column(
-                                                crossAxisAlignment: CrossAxisAlignment.start,
-                                                children: <Widget>[
-                                                  Text(
-                                                    "日均数据",
-                                                    style: Theme.of(context).textTheme.headline1.copyWith(fontSize: 16.0),
-                                                  ),
-                                                  SizedBox(
-                                                    height: 16.0,
-                                                  ),
-                                                  Container(
-                                                    padding: const EdgeInsets.fromLTRB(14.0, 21.0, 14.0, 21.0),
-                                                    decoration: card(),
-                                                    child: Column(
-                                                      children: <Widget>[
-                                                        Row(
-                                                          children: <Widget>[
-                                                            Image.asset(
-                                                              "lib/assets/img/day_icon_duration.png",
-                                                              width: 19.0,
-                                                            ),
-                                                            const SizedBox(
-                                                              width: 8.0,
-                                                            ),
-                                                            Expanded(
-                                                              child: Text(
-                                                                "日均时长",
-                                                                style: Theme.of(context).textTheme.subtitle1,
+                                                          const SizedBox(
+                                                            height: 20.0,
+                                                          ),
+                                                          Row(
+                                                            children: <Widget>[
+                                                              Image.asset(
+                                                                "lib/assets/img/day_icon_consume.png",
+                                                                width: 19.0,
                                                               ),
-                                                            ),
-                                                            SizedBox(
-                                                              width: 60.0,
-                                                              child: Text(
-                                                                "${_value?.recordsTodayAvg?.durationMinute ?? 0}分钟",
-                                                                style: Theme.of(context).textTheme.subtitle1,
+                                                              const SizedBox(
+                                                                width: 8.0,
                                                               ),
-                                                            ),
-                                                          ],
-                                                        ),
-                                                        const SizedBox(
-                                                          height: 20.0,
-                                                        ),
-                                                        Row(
-                                                          children: <Widget>[
-                                                            Image.asset(
-                                                              "lib/assets/img/day_icon_consume.png",
-                                                              width: 19.0,
-                                                            ),
-                                                            const SizedBox(
-                                                              width: 8.0,
-                                                            ),
-                                                            Expanded(
-                                                              child: Text(
-                                                                "日均消耗",
-                                                                style: Theme.of(context).textTheme.subtitle1,
+                                                              Expanded(
+                                                                child: Text(
+                                                                  "日均消耗",
+                                                                  style: Theme.of(context).textTheme.subtitle1,
+                                                                ),
                                                               ),
-                                                            ),
-                                                            SizedBox(
-                                                              width: 60.0,
-                                                              child: Text(
-                                                                "${_value?.recordsTodayAvg?.consume ?? 0}卡",
-                                                                style: Theme.of(context).textTheme.subtitle1,
+                                                              SizedBox(
+                                                                width: 60.0,
+                                                                child: Text(
+                                                                  "${_value?.recordsTodayAvg?.consume ?? 0}卡",
+                                                                  style: Theme.of(context).textTheme.subtitle1,
+                                                                ),
                                                               ),
-                                                            ),
-                                                          ],
-                                                        ),
-                                                        const SizedBox(
-                                                          height: 20.0,
-                                                        ),
-                                                        Row(
-                                                          children: <Widget>[
-                                                            Image.asset(
-                                                              "lib/assets/img/day_icon_frequency.png",
-                                                              width: 19.0,
-                                                            ),
-                                                            const SizedBox(
-                                                              width: 8.0,
-                                                            ),
-                                                            Expanded(
-                                                              child: Text(
-                                                                "日均运动次数",
-                                                                style: Theme.of(context).textTheme.subtitle1,
+                                                            ],
+                                                          ),
+                                                          const SizedBox(
+                                                            height: 20.0,
+                                                          ),
+                                                          Row(
+                                                            children: <Widget>[
+                                                              Image.asset(
+                                                                "lib/assets/img/day_icon_frequency.png",
+                                                                width: 19.0,
                                                               ),
-                                                            ),
-                                                            SizedBox(
-                                                              width: 60.0,
-                                                              child: Text(
-                                                                "${_value?.recordsTodayAvg?.times ?? 0}次",
-                                                                style: Theme.of(context).textTheme.subtitle1,
+                                                              const SizedBox(
+                                                                width: 8.0,
                                                               ),
-                                                            ),
-                                                          ],
-                                                        ),
-                                                        const SizedBox(
-                                                          height: 20.0,
-                                                        ),
-                                                        Row(
-                                                          children: <Widget>[
-                                                            Image.asset(
-                                                              "lib/assets/img/day_icon_steps.png",
-                                                              width: 19.0,
-                                                            ),
-                                                            const SizedBox(
-                                                              width: 8.0,
-                                                            ),
-                                                            Expanded(
-                                                              child: Text(
-                                                                "游戏步数",
-                                                                style: Theme.of(context).textTheme.subtitle1,
+                                                              Expanded(
+                                                                child: Text(
+                                                                  "日均运动次数",
+                                                                  style: Theme.of(context).textTheme.subtitle1,
+                                                                ),
                                                               ),
-                                                            ),
-                                                            SizedBox(
-                                                              width: 60.0,
-                                                              child: Text(
-                                                                "${_value?.recordsTodayAvg?.stepCount ?? 0}",
-                                                                style: Theme.of(context).textTheme.subtitle1,
+                                                              SizedBox(
+                                                                width: 60.0,
+                                                                child: Text(
+                                                                  "${_value?.recordsTodayAvg?.times ?? 0}次",
+                                                                  style: Theme.of(context).textTheme.subtitle1,
+                                                                ),
                                                               ),
-                                                            ),
-                                                          ],
-                                                        )
-                                                      ],
-                                                    ),
-                                                  )
-                                                ],
-                                              ),
-                                            )
-                                        ],
+                                                            ],
+                                                          ),
+                                                          const SizedBox(
+                                                            height: 20.0,
+                                                          ),
+                                                          Row(
+                                                            children: <Widget>[
+                                                              Image.asset(
+                                                                "lib/assets/img/day_icon_steps.png",
+                                                                width: 19.0,
+                                                              ),
+                                                              const SizedBox(
+                                                                width: 8.0,
+                                                              ),
+                                                              Expanded(
+                                                                child: Text(
+                                                                  "游戏步数",
+                                                                  style: Theme.of(context).textTheme.subtitle1,
+                                                                ),
+                                                              ),
+                                                              SizedBox(
+                                                                width: 60.0,
+                                                                child: Text(
+                                                                  "${_value?.recordsTodayAvg?.stepCount ?? 0}",
+                                                                  style: Theme.of(context).textTheme.subtitle1,
+                                                                ),
+                                                              ),
+                                                            ],
+                                                          )
+                                                        ],
+                                                      ),
+                                                    )
+                                                  ],
+                                                ),
+                                              )
+                                          ],
+                                        ),
                                       ),
-                                    ),
-                                  );
-                          }));
-                },
+                                    );
+                            }));
+                  },
+                ),
               ),
             ),
-          ),
-          Positioned(
-            child: SafeArea(child: buildBackButton(context)),
-          ),
-          Positioned(
-            right: 0,
-            child: SafeArea(
+            Positioned(
+              child: buildBackButton(context),
+            ),
+            Positioned(
+              right: 0,
               child: IconButton(
                 icon: Image.asset("lib/assets/img/bbs_icon_share.png"),
                 onPressed: () async {
@@ -541,8 +541,8 @@ class _PageState extends State<ConsumePage> with InjectApi {
                 },
               ),
             ),
-          ),
-        ],
+          ],
+        ),
       ),
     );
   }

+ 17 - 10
lib/pages/home/duration_page.dart

@@ -5,7 +5,6 @@ import 'package:dartin/dartin.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
-import 'package:percent_indicator/circular_percent_indicator.dart';
 import 'package:sport/bean/sport_target_index.dart';
 import 'package:sport/pages/home/duration_setting_page.dart';
 import 'package:sport/router/navigator_util.dart';
@@ -14,6 +13,7 @@ import 'package:sport/utils/date.dart';
 import 'package:sport/utils/toast.dart';
 import 'package:sport/widgets/appbar.dart';
 import 'package:sport/widgets/button_primary.dart';
+import 'package:sport/widgets/circular_percent_indicator.dart';
 import 'package:sport/widgets/decoration.dart';
 import 'package:sport/widgets/dialog/request_dialog.dart';
 import 'package:sport/widgets/image.dart';
@@ -21,6 +21,9 @@ import 'package:sport/widgets/loading.dart';
 import 'package:sport/widgets/misc.dart';
 import 'package:sport/widgets/space.dart';
 
+var _succColor = const Color(0xff5498FF);
+var _failColor = const Color(0xffFF5B1D);
+
 class DurationPage extends StatefulWidget {
   @override
   State<StatefulWidget> createState() => _PageState();
@@ -48,12 +51,12 @@ class _PageState extends State<DurationPage> {
     var _dot = Container(
       width: 5,
       height: 5,
-      decoration: BoxDecoration(shape: BoxShape.circle, color: Color(0xff5498FF)),
+      decoration: BoxDecoration(shape: BoxShape.circle, color: _succColor),
     );
     var _dotFailed = Container(
       width: 5,
       height: 5,
-      decoration: BoxDecoration(shape: BoxShape.circle, color: Color(0xffFF5B1D)),
+      decoration: BoxDecoration(shape: BoxShape.circle, color: _failColor),
     );
     return Scaffold(
       body: FutureBuilder(
@@ -90,12 +93,12 @@ class _PageState extends State<DurationPage> {
                             child: CircularPercentIndicator(
                               radius: 140.0,
                               lineWidth: 12.0,
-                              percent: min(1.0, 1.0 * (snapshot.data?.today?.value ?? 0) / snapshot.data?.today?.valueTarget ?? 0.01),
+                              percent: 1.0 * (snapshot.data?.today?.value ?? 0) / snapshot.data?.today?.valueTarget ?? 0.01,
                               center: Column(
                                 children: <Widget>[
                                   Text(
                                     "${snapshot.data?.today?.value ?? 0}",
-                                    style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 40.0),
+                                    style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 40.0, fontFamily: "DIN"),
                                   ),
                                   Text(
                                     "${snapshot.data?.today?.label}",
@@ -108,7 +111,8 @@ class _PageState extends State<DurationPage> {
                               animation: true,
                               animationDuration: 1000,
                               animateFromLastPercent: true,
-                              startAngle: 5,
+                              startAngle: 200.0,
+                              arcType: ArcType.CUSTOM,
                               backgroundColor: Color(0xfff1f1f1),
                               circularStrokeCap: CircularStrokeCap.round,
                               rotateLinearGradient: true,
@@ -123,7 +127,7 @@ class _PageState extends State<DurationPage> {
                           Padding(
                             padding: const EdgeInsets.symmetric(vertical: 12.0),
                             child: Text(
-                              "运动目标:${snapshot.data?.today?.value ?? 0} ${snapshot.data?.today?.label}",
+                              "运动目标:${snapshot.data?.target?.valueTarget ?? 0} ${snapshot.data?.today?.label}",
                               style: Theme.of(context).textTheme.subtitle1,
                             ),
                           ),
@@ -141,11 +145,11 @@ class _PageState extends State<DurationPage> {
                                       snapshot.data?.today?.finish == true
                                           ? Text(
                                               "已达标",
-                                              style: Theme.of(context).textTheme.bodyText2.copyWith(color: Color(0xff6BC93F)),
+                                              style: Theme.of(context).textTheme.bodyText2.copyWith(color: _succColor),
                                             )
                                           : Text(
                                               "未达标",
-                                              style: Theme.of(context).textTheme.bodyText2.copyWith(color: Color(0xffFF5B1D)),
+                                              style: Theme.of(context).textTheme.bodyText2.copyWith(color: _failColor),
                                             )
                                     ],
                                     mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -438,7 +442,10 @@ class _PageState extends State<DurationPage> {
                               mainAxisSpacing: 12.0,
                               staggeredTileBuilder: (int index) => StaggeredTile.fit(1),
                             ),
-                          )
+                          ),
+                          const SizedBox(
+                            height: 50.0,
+                          ),
                         ],
                       ),
                     ),

+ 31 - 34
lib/pages/home/duration_setting_page.dart

@@ -10,6 +10,7 @@ import 'package:sport/widgets/appbar.dart';
 import 'package:sport/widgets/button_primary.dart';
 import 'package:sport/widgets/dialog/alert_dialog.dart';
 import 'package:sport/widgets/dialog/request_dialog.dart';
+import 'package:sport/widgets/misc.dart';
 
 class DurationSettingPage extends StatefulWidget {
   @override
@@ -22,9 +23,10 @@ class _PageState extends State<DurationSettingPage> with SingleTickerProviderSta
   @override
   void initState() {
     super.initState();
-    _controller = new TabController(length: 2, vsync: this);
-    _controller.addListener(() {
-//      print(_controller.index);
+    _controller = new TabController(length: 2, vsync: this)..addListener(() {
+      setState(() {
+
+      });
     });
   }
 
@@ -38,42 +40,36 @@ class _PageState extends State<DurationSettingPage> with SingleTickerProviderSta
   Widget build(BuildContext context) {
     return Scaffold(
       backgroundColor: Colors.white,
-      appBar: AppBar(
-        titleSpacing: -12.0,
-        centerTitle: false,
-        title: Text(
-          "设定目标",
-          style: titleStyle,
-        ),
-        leading: buildBackButton(context),
-      ),
+      appBar: buildAppBar(context, title: "设定目标"),
       body: Padding(
         padding: const EdgeInsets.all(12.0),
         child: Column(
           children: <Widget>[
-            TabBar(
-              controller: _controller,
-              isScrollable: true,
-              indicatorPadding: EdgeInsets.symmetric(horizontal: 10.0),
-              indicatorWeight: 3,
-              unselectedLabelColor: Color(0xff999999),
-              labelStyle: TextStyle(fontSize: 16.0),
-              unselectedLabelStyle: TextStyle(fontSize: 16.0),
-              labelPadding: EdgeInsets.symmetric(vertical: 0.0, horizontal: 30.0),
+            Center(
+              child: TabBar(
+                controller: _controller,
+                isScrollable: true,
+                indicatorPadding: EdgeInsets.symmetric(horizontal: 10.0),
+                indicatorWeight: 3,
+                unselectedLabelColor: Color(0xff999999),
+                labelStyle: TextStyle(fontSize: 16.0),
+                unselectedLabelStyle: TextStyle(fontSize: 16.0),
+                labelPadding: EdgeInsets.symmetric(vertical: 0.0, horizontal: 30.0),
 //          indicator: const BoxDecoration(),
 //          labelPadding:  EdgeInsets.all(20.0),
-              tabs: <Widget>[
-                Tab(
-                  icon: Image.asset("lib/assets/img/setgoals_icon_duration_${_controller.index == 0 ? "press" : "normal"}.png"),
-                  text: "时长",
-                  iconMargin: EdgeInsets.only(bottom: 3.0),
-                ),
-                Tab(
-                  icon: Image.asset("lib/assets/img/setgoals_icon_consume_${_controller.index == 1 ? "press" : "normal"}.png"),
-                  text: "卡路里",
-                  iconMargin: EdgeInsets.only(bottom: 3.0),
-                )
-              ],
+                tabs: <Widget>[
+                  Tab(
+                    icon: Image.asset("lib/assets/img/setgoals_icon_duration_${_controller.index == 0 ? "press" : "normal"}.png"),
+                    text: "时长",
+                    iconMargin: EdgeInsets.only(bottom: 3.0),
+                  ),
+                  Tab(
+                    icon: Image.asset("lib/assets/img/setgoals_icon_consume_${_controller.index == 1 ? "press" : "normal"}.png"),
+                    text: "卡路里",
+                    iconMargin: EdgeInsets.only(bottom: 3.0),
+                  )
+                ],
+              ),
             ),
             Divider(),
             Expanded(
@@ -81,6 +77,7 @@ class _PageState extends State<DurationSettingPage> with SingleTickerProviderSta
                 padding: const EdgeInsets.only(top: 30.0),
                 child: TabBarView(
                   controller: _controller,
+                  physics: NeverScrollableScrollPhysics(),
                   children: <Widget>[
                     _Form(type: 0, unit: "分钟", items: [20, 40, 60, 80, 100, 120]),
                     _Form(type: 1, unit: "卡", items: [100, 150, 200, 250, 300, 350]),
@@ -194,7 +191,7 @@ class _FormState extends State<_Form> {
                 width: .5,
               ),
             ),
-            child: Center(child: Text("自定义", style: Theme.of(context).textTheme.subtitle1.copyWith(color: Theme.of(context).accentColor),strutStyle: StrutStyle(forceStrutHeight: true),)),
+            child: Center(child: Text("自定义", style: Theme.of(context).textTheme.subtitle1.copyWith(color: Theme.of(context).accentColor), strutStyle: fixedLine,)),
           ),
         ),
         const SizedBox(

+ 8 - 7
lib/pages/home/home_info_page.dart

@@ -4,7 +4,6 @@ import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_blue/flutter_blue.dart';
 import 'package:flutter_easyrefresh/easy_refresh.dart';
-import 'package:percent_indicator/circular_percent_indicator.dart';
 import 'package:provider/provider.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:sport/bean/game.dart' as game;
@@ -32,6 +31,7 @@ import 'package:sport/provider/user_model.dart';
 import 'package:sport/router/navigator_util.dart';
 import 'package:sport/router/routes.dart';
 import 'package:sport/widgets/button_primary.dart';
+import 'package:sport/widgets/circular_percent_indicator.dart';
 import 'package:sport/widgets/decoration.dart';
 import 'package:sport/widgets/dialog/search_device.dart';
 import 'package:sport/widgets/dialog/share_popup.dart';
@@ -238,7 +238,7 @@ class _PageState extends ViewStateLifecycle<HomeInfoPage, SportIndexModel> with
                                     children: <Widget>[
                                       Text(
                                         "${model.data?.today?.step ?? 0}",
-                                        style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0),
+                                        style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0, fontFamily: "DIN"),
                                         strutStyle: fixedLine,
                                       ),
                                       Text(
@@ -288,7 +288,7 @@ class _PageState extends ViewStateLifecycle<HomeInfoPage, SportIndexModel> with
                                     children: <Widget>[
                                       Text(
                                         "${model.data?.today?.consume ?? 0}",
-                                        style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0),
+                                        style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0, fontFamily: "DIN"),
                                         strutStyle: fixedLine,
                                       ),
                                       Text(
@@ -352,7 +352,7 @@ class _PageState extends ViewStateLifecycle<HomeInfoPage, SportIndexModel> with
                               child: CircularPercentIndicator(
                                 radius: 100.0,
                                 lineWidth: 10.0,
-                                percent: min(1.0, 1.0 * (model?.data?.today?.value(model.data?.target?.type) ?? 0) / (model.data?.target?.valueTarget ?? 0.01)),
+                                percent: 1.0 * (model?.data?.today?.value(model.data?.target?.type) ?? 0) / (model.data?.target?.valueTarget ?? 0.01),
                                 center: Column(
                                   children: <Widget>[
                                     SizedBox(
@@ -360,7 +360,7 @@ class _PageState extends ViewStateLifecycle<HomeInfoPage, SportIndexModel> with
                                     ),
                                     Text(
                                       "${model.data?.today?.value(model.data?.target?.type) ?? 0}",
-                                      style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0),
+                                      style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0, fontFamily: "DIN"),
                                       strutStyle: fixedLine,
                                     ),
                                     Text(
@@ -374,7 +374,8 @@ class _PageState extends ViewStateLifecycle<HomeInfoPage, SportIndexModel> with
                                 animation: true,
                                 animationDuration: 1000,
                                 animateFromLastPercent: true,
-                                startAngle: 5,
+                                startAngle: 200.0,
+                                arcType: ArcType.CUSTOM,
                                 backgroundColor: Color(0xfff1f1f1),
                                 circularStrokeCap: CircularStrokeCap.round,
                                 rotateLinearGradient: true,
@@ -454,7 +455,7 @@ class _PageState extends ViewStateLifecycle<HomeInfoPage, SportIndexModel> with
                                   ),
                                   Text(
                                     "${model.data?.today?.strength?.toStringAsFixed(1) ?? 0}",
-                                    style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0),
+                                    style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0, fontFamily: "DIN"),
                                     strutStyle: fixedLine,
                                   ),
                                   Text(

+ 2 - 1
lib/pages/home/sport_history_all_page.dart

@@ -72,13 +72,14 @@ class _PageState extends ViewStateLifecycle<SportHistoryAllPage, SimpleModel> wi
                                 return GestureDetector(
                                   onTap: () => NavigatorUtil.goPage(context, (context) => SportHistoryPage(item.game)),
                                   child: Container(
-                                      margin: const EdgeInsets.only(top: ui_padding),
+                                      margin: index == 0 ?const EdgeInsets.only(top: 0) :const EdgeInsets.only(top: ui_padding),
                                       decoration: circular(),
                                       child: Column(
                                         children: <Widget>[
                                           Padding(
                                             padding: const EdgeInsets.fromLTRB(ui_padding, 14.0, ui_padding, 14.0),
                                             child: Row(
+                                              crossAxisAlignment: CrossAxisAlignment.start,
                                               children: <Widget>[
                                                 GestureDetector(
                                                   onTap: () => NavigatorUtil.goPage(context, (context) => GameDetailsPage(item.game)),

+ 132 - 126
lib/pages/home/sport_list_page.dart

@@ -1,11 +1,11 @@
 import 'dart:ui';
 
 import 'package:cached_network_image/cached_network_image.dart';
-import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_rating_bar/flutter_rating_bar.dart';
 import 'package:sport/bean/game.dart';
 import 'package:sport/pages/game/game_detail.dart';
+import 'package:sport/router/navigator_util.dart';
 import 'package:sport/services/api/inject_api.dart';
 import 'package:sport/services/api/resp.dart';
 import 'package:sport/widgets/decoration.dart';
@@ -37,140 +37,146 @@ class _PageState extends State<SportListPage> with InjectApi {
       filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
       child: Scaffold(
         backgroundColor: Colors.transparent.withOpacity(0.5),
-        body: Center(
-          child: Padding(
-            padding: const EdgeInsets.fromLTRB(0, 80.0, 0, 20.0),
-            child: Column(
-              children: <Widget>[
-                Padding(
-                  padding: const EdgeInsets.all(12.0),
-                  child: Stack(
-                    alignment: Alignment.center,
-                    children: <Widget>[
-                      Image.asset(
-                        "lib/assets/img/mine_image_achievement.png",
-                        fit: BoxFit.fitWidth,
-                        width: 240,
-                      ),
-                      Text(
-                        "运动列表",
-                        style: Theme.of(context).textTheme.headline4,
-                        strutStyle: fixedLine,
-                      )
-                    ],
+        body: GestureDetector(
+          onTap: () => Navigator.maybePop(context),
+          behavior: HitTestBehavior.opaque,
+          child: Center(
+            child: Padding(
+              padding: const EdgeInsets.fromLTRB(0, 80.0, 0, 20.0),
+              child: Column(
+                children: <Widget>[
+                  Padding(
+                    padding: const EdgeInsets.all(12.0),
+                    child: Stack(
+                      alignment: Alignment.center,
+                      children: <Widget>[
+                        Image.asset(
+                          "lib/assets/img/mine_image_achievement.png",
+                          fit: BoxFit.fitWidth,
+                          width: 240,
+                        ),
+                        Text(
+                          "运动列表",
+                          style: Theme.of(context).textTheme.headline4,
+                          strutStyle: fixedLine,
+                        )
+                      ],
+                    ),
                   ),
-                ),
-                Expanded(
-                  child: SingleChildScrollView(
-                    child: Padding(
-                      padding: const EdgeInsets.all(12.0),
-                      child: Column(
-                        children: <Widget>[
-                          Container(
-                            margin: _padding,
-                            child: GameRun(),
-                            decoration: circular(),
-                          ),
-                          FutureBuilder(
-                            future: _future,
-                            builder: (BuildContext context, AsyncSnapshot<RespList<GameInfoData>> snapshot) {
-                              if (snapshot.connectionState != ConnectionState.done) return RequestLoadingWidget();
-                              var list = snapshot.data?.results;
-                              return Column(
-                                  children: list
-                                      .map((e) => Padding(
+                  Expanded(
+                    child: SingleChildScrollView(
+                      child: Padding(
+                        padding: const EdgeInsets.all(12.0),
+                        child: Column(
+                          children: <Widget>[
+                            Container(
+                              margin: _padding,
+                              child: GameRun(),
+                              decoration: circular(),
+                            ),
+                            FutureBuilder(
+                              future: _future,
+                              builder: (BuildContext context, AsyncSnapshot<RespList<GameInfoData>> snapshot) {
+                                if (snapshot.connectionState != ConnectionState.done) return RequestLoadingWidget();
+                                var list = snapshot.data?.results;
+                                return Column(
+                                    children: list
+                                        .map((e) => Padding(
                                             padding: _padding,
-                                            child: ClipRRect(
-                                                borderRadius: BorderRadius.circular(10.0),
-                                                child: Container(
-                                                  color: Colors.white,
-                                                  child: Row(
-                                                    children: <Widget>[
-                                                      CachedNetworkImage(
-                                                        width: 90.0,
-                                                        height: 90.0,
-                                                        fit: BoxFit.cover,
-                                                        imageUrl: "${e.cover}",
-                                                      ),
-                                                      const SizedBox(
-                                                        width: 12.0,
-                                                      ),
-                                                      Expanded(
-                                                        child: Column(
-                                                          crossAxisAlignment: CrossAxisAlignment.start,
-                                                          children: <Widget>[
-                                                            Text(
-                                                              e.name,
-                                                              style: Theme.of(context).textTheme.headline3,
-                                                            ),
-                                                            const SizedBox(
-                                                              height: 3,
-                                                            ),
-                                                            Text(
-                                                              "${e.userCount}人在练",
-                                                              style: Theme.of(context).textTheme.bodyText1,
-                                                            ),
-                                                            const SizedBox(
-                                                              height: 5,
-                                                            ),
-                                                            Row(
-                                                              children: <Widget>[
-                                                                Text(
-                                                                  "难度: ",
-                                                                  style: Theme.of(context).textTheme.bodyText1,
-                                                                ),
-                                                                RatingBarIndicator(
-                                                                  rating: e.difficulty / 20.0,
-                                                                  itemBuilder: (context, index) => Image.asset(
-                                                                    "lib/assets/img/con_icon_difficulty_normal.png",
-                                                                    color: const Color(0xffFFC400),
-                                                                  ),
-                                                                  itemCount: 5,
-                                                                  itemSize: 14.0,
-                                                                  unratedColor: const Color(0xffCECECE),
-                                                                  direction: Axis.horizontal,
-                                                                )
-                                                              ],
-                                                            )
-                                                          ],
+                                            child: GestureDetector(
+                                              onTap: () => NavigatorUtil.goPage(context, (context) => GameDetailsPage(e)),
+                                              child: ClipRRect(
+                                                  borderRadius: BorderRadius.circular(10.0),
+                                                  child: Container(
+                                                    color: Colors.white,
+                                                    child: Row(
+                                                      children: <Widget>[
+                                                        CachedNetworkImage(
+                                                          width: 90.0,
+                                                          height: 90.0,
+                                                          fit: BoxFit.cover,
+                                                          imageUrl: "${e.cover}",
+                                                        ),
+                                                        const SizedBox(
+                                                          width: 12.0,
                                                         ),
-                                                      ),
-                                                      Container(
-                                                        width: 120.0,
-                                                        child: PositionedBottom(
-                                                          e,
-                                                          (v) {
-                                                            startGame(context, e);
-                                                          },
-                                                          height: 35.0,
-                                                          width: 93.0,
-                                                          textStyle: TextStyle(
-                                                            color: Colors.white,
-                                                            fontSize: 14.0,
+                                                        Expanded(
+                                                          child: Column(
+                                                            crossAxisAlignment: CrossAxisAlignment.start,
+                                                            children: <Widget>[
+                                                              Text(
+                                                                e.name,
+                                                                style: Theme.of(context).textTheme.headline3,
+                                                              ),
+                                                              const SizedBox(
+                                                                height: 3,
+                                                              ),
+                                                              Text(
+                                                                "${e.userCount}人在练",
+                                                                style: Theme.of(context).textTheme.bodyText1,
+                                                              ),
+                                                              const SizedBox(
+                                                                height: 5,
+                                                              ),
+                                                              Row(
+                                                                children: <Widget>[
+                                                                  Text(
+                                                                    "难度: ",
+                                                                    style: Theme.of(context).textTheme.bodyText1,
+                                                                  ),
+                                                                  RatingBarIndicator(
+                                                                    rating: e.difficulty / 20.0,
+                                                                    itemBuilder: (context, index) => Image.asset(
+                                                                      "lib/assets/img/con_icon_difficulty_normal.png",
+                                                                      color: const Color(0xffFFC400),
+                                                                    ),
+                                                                    itemCount: 5,
+                                                                    itemSize: 14.0,
+                                                                    unratedColor: const Color(0xffCECECE),
+                                                                    direction: Axis.horizontal,
+                                                                  )
+                                                                ],
+                                                              )
+                                                            ],
                                                           ),
                                                         ),
-                                                      )
-                                                    ],
-                                                  ),
-                                                )),
-                                          ))
-                                      .toList());
-                            },
-                          )
-                        ],
+                                                        Container(
+                                                          width: 120.0,
+                                                          child: PositionedBottom(
+                                                            e,
+                                                            (v) {
+                                                              startGame(context, e);
+                                                            },
+                                                            height: 35.0,
+                                                            width: 93.0,
+                                                            textStyle: TextStyle(
+                                                              color: Colors.white,
+                                                              fontSize: 14.0,
+                                                            ),
+                                                          ),
+                                                        )
+                                                      ],
+                                                    ),
+                                                  )),
+                                            )))
+                                        .toList());
+                              },
+                            )
+                          ],
+                        ),
                       ),
                     ),
                   ),
-                ),
-                GestureDetector(
-                  behavior: HitTestBehavior.opaque,
-                  onTap: () => Navigator.maybePop(context),
-                  child: Padding(
-                    padding: const EdgeInsets.all(8.0),
-                    child: Image.asset("lib/assets/img/pop_share_chose.png"),
+                  GestureDetector(
+                    behavior: HitTestBehavior.opaque,
+                    onTap: () => Navigator.maybePop(context),
+                    child: Padding(
+                      padding: const EdgeInsets.all(8.0),
+                      child: Image.asset("lib/assets/img/pop_share_chose.png"),
+                    ),
                   ),
-                ),
-              ],
+                ],
+              ),
             ),
           ),
         ),

+ 29 - 27
lib/pages/home/step_page.dart

@@ -12,6 +12,7 @@ import 'package:sport/services/Converter.dart';
 import 'package:sport/services/api/inject_api.dart';
 import 'package:sport/services/api/resp.dart';
 import 'package:sport/utils/date.dart';
+import 'package:sport/utils/toast.dart';
 import 'package:sport/widgets/appbar.dart';
 import 'package:sport/widgets/chart.dart';
 import 'package:sport/widgets/image.dart';
@@ -58,14 +59,9 @@ class _PageState extends State<StepPage> with TickerProviderStateMixin, InjectAp
   Widget build(BuildContext context) {
     return Scaffold(
         backgroundColor: Colors.white,
-        appBar: AppBar(
-          titleSpacing: -12.0,
-          centerTitle: false,
-          title: Text(
-            "运动步数",
-            style: titleStyle,
-          ),
-          leading: buildBackButton(context),
+        appBar: buildAppBar(
+          context,
+          title: "运动步数",
           actions: <Widget>[
             IconButton(
               icon: Image.asset(
@@ -160,6 +156,7 @@ class _PageState extends State<StepPage> with TickerProviderStateMixin, InjectAp
                                 behavior: HitTestBehavior.opaque,
                                 onTap: () {
                                   if (_pageController?.page == 0.0) {
+                                    ToastUtil.show("没有数据了");
                                     return;
                                   }
                                   _pageController?.previousPage(duration: Duration(milliseconds: 500), curve: Curves.linear);
@@ -191,7 +188,7 @@ class _PageState extends State<StepPage> with TickerProviderStateMixin, InjectAp
                               return snapshot.connectionState != ConnectionState.done
                                   ? RequestLoadingWidget()
                                   : Padding(
-                                      padding:const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 0),
+                                      padding: const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 0),
                                       child: SingleChildScrollView(
                                         child: Column(
                                           children: <Widget>[
@@ -201,7 +198,7 @@ class _PageState extends State<StepPage> with TickerProviderStateMixin, InjectAp
                                             ),
                                             Text(
                                               toType() == 0 ? "日总步数" : "日均步数",
-                                              style: Theme.of(context).textTheme.subtitle2.copyWith(color:Color(0xff999999)),
+                                              style: Theme.of(context).textTheme.subtitle2.copyWith(color: Color(0xff999999)),
                                             ),
                                             SizedBox(
                                               height: 30.0,
@@ -211,7 +208,10 @@ class _PageState extends State<StepPage> with TickerProviderStateMixin, InjectAp
                                               child: CustomPaint(
                                                 painter: Chart(
                                                     type: TABS.indexOf(_tab.value),
-                                                    records: snapshot.data?.data?.records?.map((e) => ChartItem(type == 3 ? "${e.month}": e.createdAt, e.step))?.toList() ?? [],
+                                                    records: snapshot.data?.data?.records
+                                                            ?.map((e) => ChartItem(type == 3 ? "${e.month}" : e.createdAt, e.step))
+                                                            ?.toList() ??
+                                                        [],
                                                     dateTime: time,
                                                     drawMax: true,
                                                     unit: "歩")
@@ -239,7 +239,8 @@ class _PageState extends State<StepPage> with TickerProviderStateMixin, InjectAp
                                                   RichText(
                                                     text: TextSpan(style: Theme.of(context).textTheme.subtitle2, children: <InlineSpan>[
                                                       TextSpan(
-                                                          text: '${_value == null ? ".." : "${((_value.sum.stepDaily + _value.sum.stepGame) * .6).toStringAsFixed(1)}"}',
+                                                          text:
+                                                              '${_value == null ? ".." : "${((_value.sum.stepDaily + _value.sum.stepGame) * .6).toStringAsFixed(1)}"}',
                                                           style: Theme.of(context).textTheme.subtitle2.copyWith(fontWeight: FontWeight.bold, fontSize: 25)),
                                                       TextSpan(text: ' 米', style: Theme.of(context).textTheme.subtitle1),
                                                     ]),
@@ -247,21 +248,22 @@ class _PageState extends State<StepPage> with TickerProviderStateMixin, InjectAp
                                                   Divider(
                                                     height: 24,
                                                   ),
-                                                  if(type != 0)
-                                                  Row(
-                                                    mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                                                    children: <Widget>[
-                                                      Text(
-                                                        type == 1? "周总步数" : type ==2?"月总步数":"年总步数",
-                                                        style: Theme.of(context).textTheme.subtitle1,
-                                                      ),
-                                                      Text("${_value == null ? ".." : "${_value.sum.stepDaily + _value.sum.stepGame}"}", style: Theme.of(context).textTheme.subtitle1)
-                                                    ],
-                                                  ),
-                                                  if(type != 0)
-                                                  Space(
-                                                    height: 8,
-                                                  ),
+                                                  if (type != 0)
+                                                    Row(
+                                                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                                                      children: <Widget>[
+                                                        Text(
+                                                          type == 1 ? "周总步数" : type == 2 ? "月总步数" : "年总步数",
+                                                          style: Theme.of(context).textTheme.subtitle1,
+                                                        ),
+                                                        Text("${_value == null ? ".." : "${_value.sum.stepDaily + _value.sum.stepGame}"}",
+                                                            style: Theme.of(context).textTheme.subtitle1)
+                                                      ],
+                                                    ),
+                                                  if (type != 0)
+                                                    Space(
+                                                      height: 8,
+                                                    ),
                                                   Row(
                                                     mainAxisAlignment: MainAxisAlignment.spaceBetween,
                                                     children: <Widget>[

+ 1 - 9
lib/pages/home/step_realtime_page.dart

@@ -115,15 +115,7 @@ class _PageState extends State<StepRealTimePage> {
   Widget build(BuildContext context) {
     return Scaffold(
         backgroundColor: Colors.white,
-        appBar: AppBar(
-          titleSpacing: 0,
-          centerTitle: false,
-          title: Text(
-            "实时计步",
-            style: titleStyle,
-          ),
-          leading: buildBackButton(context),
-        ),
+        appBar: buildAppBar(context, title: "实时计步"),
         body: Column(
           children: <Widget>[
             Flexible(

+ 9 - 7
lib/pages/home/strength_page.dart

@@ -48,8 +48,8 @@ class _PageState extends State<StrengthPage> with InjectApi {
           buildSliverAppBar(context, "运动强度", backgroundColor: Theme.of(context).scaffoldBackgroundColor),
           SliverToBoxAdapter(
             child: Container(
-              margin: const EdgeInsets.all(12.0),
-              padding: const EdgeInsets.all(12.0),
+              margin: const EdgeInsets.symmetric(horizontal: 12.0),
+              padding: const EdgeInsets.only(top:20.0,bottom: 10.0,left: 16.0,right: 16.0),
               decoration: circular(),
               child: FutureBuilder(
                 future: createFutureType(0),
@@ -68,12 +68,14 @@ class _PageState extends State<StrengthPage> with InjectApi {
                               center: Column(
                                 children: <Widget>[
                                   SizedBox(
-                                    height: 10.0,
+                                    height: 16.0,
                                   ),
                                   Text(
                                     "${strengthToValue(snapshot?.data?.data?.sum?.consume ?? 0).toStringAsFixed(1)}",
-                                    style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0),
+                                    style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 40.0, fontFamily: "DIN"),
                                     strutStyle: fixedLine,
+                                  ),SizedBox(
+                                    height: 5.0,
                                   ),
                                   Text(
                                     "卡/分",
@@ -164,7 +166,7 @@ class _PageState extends State<StrengthPage> with InjectApi {
                             children: <Widget>[
                               Text(
                                 "${snapshot?.data?.data?.sum?.consume ?? 0}",
-                                style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0),
+                                style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0, fontFamily: "DIN"),
                               ),
                               Space(height: 4.0,),
                               Text(
@@ -179,7 +181,7 @@ class _PageState extends State<StrengthPage> with InjectApi {
                             children: <Widget>[
                               Text(
                                 "${snapshot?.data?.data?.sum?.durationMinute ?? 0}",
-                                style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0),
+                                style: Theme.of(context).textTheme.headline2.copyWith(fontSize: 26.0, fontFamily: "DIN"),
                               ),
                               Space(height: 4.0,),
                               Text(
@@ -477,7 +479,7 @@ class _Dot extends CustomPainter {
     final double cx = size.width / 2;
     final double progress = min(1.0, items[index].data / 60.0 / 12.0);
     final double cy = _height * (1.0 - progress);
-    print("$progress $cy ${items[index].data}");
+    // print("$progress $cy ${items[index].data}");
     final double radius = size.width * 0.15 / 2;
     _paint.color = _color;
     canvas.drawCircle(Offset(cx, cy), radius, _paint);

+ 49 - 46
lib/pages/home_page.dart

@@ -60,6 +60,7 @@ class _HomePageState extends LifecycleState<HomePage> with ConfigInject {
       default:
         break;
     }
+    Provider.of<MessageModel>(context, listen: false).state = state;
   }
 
   @override
@@ -86,57 +87,59 @@ class _HomePageState extends LifecycleState<HomePage> with ConfigInject {
       value: SystemUiOverlayStyle.light,
       child: Scaffold(
         backgroundColor: Colors.white,
-        bottomNavigationBar: SizedBox(
-          height: 49,
-          child: Container(
-            child: ValueListenableBuilder(
-              valueListenable: _valueNotifierIndex,
-              builder: (BuildContext context, int value, Widget child) => CupertinoTabBar(
-                backgroundColor: const Color(0xff1D1D1D),
-                border: null,
-                iconSize: 22.0,
-                activeColor: const Color(0xffFFC400),
-                inactiveColor: const Color(0xff666666),
-                items: <BottomNavigationBarItem>[
-                  BottomNavigationBarItem(
-                    icon: Padding(
-                      padding: const EdgeInsets.only(top: 6.0),
-                      child: Image.asset("lib/assets/img/tab_home_${value == 0 ? "press" : "normal"}.png"),
+        bottomNavigationBar: SafeArea(
+          child: SizedBox(
+            height: 49,
+            child: Container(
+              child: ValueListenableBuilder(
+                valueListenable: _valueNotifierIndex,
+                builder: (BuildContext context, int value, Widget child) => CupertinoTabBar(
+                  backgroundColor: const Color(0xff1D1D1D),
+                  border: null,
+                  iconSize: 22.0,
+                  activeColor: const Color(0xffFFC400),
+                  inactiveColor: const Color(0xff666666),
+                  items: <BottomNavigationBarItem>[
+                    BottomNavigationBarItem(
+                      icon: Padding(
+                        padding: const EdgeInsets.only(top: 6.0),
+                        child: Image.asset("lib/assets/img/tab_home_${value == 0 ? "press" : "normal"}.png"),
+                      ),
+                      title: Text(
+                        "首页",
+                        style: TextStyle(fontSize: 11.0),
+                      ),
                     ),
-                    title: Text(
-                      "首页",
-                      style: TextStyle(fontSize: 11.0),
+                    BottomNavigationBarItem(
+                      icon: Padding(
+                        padding: const EdgeInsets.only(top: 6.0),
+                        child: Image.asset("lib/assets/img/tab_game_${value == 1 ? "press" : "normal"}.png"),
+                      ),
+                      title: Text("运动", style: TextStyle(fontSize: 11.0)),
                     ),
-                  ),
-                  BottomNavigationBarItem(
-                    icon: Padding(
-                      padding: const EdgeInsets.only(top: 6.0),
-                      child: Image.asset("lib/assets/img/tab_game_${value == 1 ? "press" : "normal"}.png"),
+                    BottomNavigationBarItem(
+                      icon: Padding(
+                        padding: const EdgeInsets.only(top: 6.0),
+                        child: Image.asset("lib/assets/img/tab_bbs_${value == 2 ? "press" : "normal"}.png"),
+                      ),
+                      title: Text("社区", style: TextStyle(fontSize: 11.0)),
                     ),
-                    title: Text("运动", style: TextStyle(fontSize: 11.0)),
-                  ),
-                  BottomNavigationBarItem(
-                    icon: Padding(
-                      padding: const EdgeInsets.only(top: 6.0),
-                      child: Image.asset("lib/assets/img/tab_bbs_${value == 2 ? "press" : "normal"}.png"),
+                    BottomNavigationBarItem(
+                      icon: Padding(
+                        padding: const EdgeInsets.only(top: 6.0),
+                        child: Image.asset("lib/assets/img/tab_my_${value == 3 ? "press" : "normal"}.png"),
+                      ),
+                      title: Text("我的", style: TextStyle(fontSize: 11.0)),
                     ),
-                    title: Text("社区", style: TextStyle(fontSize: 11.0)),
-                  ),
-                  BottomNavigationBarItem(
-                    icon: Padding(
-                      padding: const EdgeInsets.only(top: 6.0),
-                      child: Image.asset("lib/assets/img/tab_my_${value == 3 ? "press" : "normal"}.png"),
-                    ),
-                    title: Text("我的", style: TextStyle(fontSize: 11.0)),
-                  ),
-                ],
-                currentIndex: value,
-                onTap: (index) {
-                  _pageController.jumpToPage(index);
-                  _valueNotifierIndex.value = index;
-                  // 这里得轮询解决
+                  ],
+                  currentIndex: value,
+                  onTap: (index) {
+                    _pageController.jumpToPage(index);
+                    _valueNotifierIndex.value = index;
+                    // 这里得轮询解决
 //
-                },
+                  },
+                ),
               ),
             ),
           ),

+ 4 - 1
lib/pages/my/achievement_detail_page.dart

@@ -521,7 +521,10 @@ Future<bool> showSharePopup(
                                                 "${relateAchievements[value].name}",
                                                 style: TextStyle(
                                                     fontSize: 18.0,
-                                                    color: Colors.white),
+                                                    color: Colors.white,
+                                                    fontWeight:
+                                                    FontWeight.bold
+                                                ),
                                               ),
                                               Space(
                                                 height: 10.0,

+ 343 - 188
lib/pages/my/feedback_page.dart

@@ -40,13 +40,17 @@ class _PageState extends State<FeedbackPage> with InjectLoginApi, InjectApi {
   FocusNode _focusNode;
   var _posting = false;
   int joinTime = 0;
+  ValueNotifier<bool> _postable = ValueNotifier(false);
 
   @override
   void initState() {
     initData();
 
     _focusNode = FocusNode();
-    _controller = TextEditingController();
+    _controller = TextEditingController()
+      ..addListener(() {
+        _postable.value = _controller.value.text.isNotEmpty;
+      });
 
     _isLoading = true;
     super.initState();
@@ -85,7 +89,10 @@ class _PageState extends State<FeedbackPage> with InjectLoginApi, InjectApi {
       list.insert(index, FeedBackInfoData(from: "-1", createTime: now));
       list.insert(index, FeedBackInfoData(from: "-2", createTime: now));
     } else {
-      list = [FeedBackInfoData(from: "-1", createTime: now), FeedBackInfoData(from: "-2", createTime: now)];
+      list = [
+        FeedBackInfoData(from: "-1", createTime: now),
+        FeedBackInfoData(from: "-2", createTime: now)
+      ];
       joinTime = now;
     }
 
@@ -96,7 +103,8 @@ class _PageState extends State<FeedbackPage> with InjectLoginApi, InjectApi {
       if (t == 0) {
         t = e.createTime;
       }
-      print("$t ${e.createTime} ${day} ${e.createTime + day} ${t > e.createTime + day}");
+      print(
+          "$t ${e.createTime} ${day} ${e.createTime + day} ${t > e.createTime + day}");
       if (t > e.createTime + day) {
         fixed.add(FeedBackInfoData(from: "-3", createTime: t));
       }
@@ -104,7 +112,8 @@ class _PageState extends State<FeedbackPage> with InjectLoginApi, InjectApi {
       t = e.createTime;
     }
     if (fixed.length > 2) {
-      fixed.add(FeedBackInfoData(from: "-3", createTime: fixed[fixed.length - 1].createTime));
+      fixed.add(FeedBackInfoData(
+          from: "-3", createTime: fixed[fixed.length - 1].createTime));
     }
 
     data.data = fixed;
@@ -117,13 +126,7 @@ class _PageState extends State<FeedbackPage> with InjectLoginApi, InjectApi {
   @override
   Widget build(BuildContext context) {
     return Scaffold(
-        appBar: AppBar(
-          title: Text(
-            "用户反馈",
-            style: titleStyle,
-          ),
-          leading: buildBackButton(context),
-        ),
+        appBar: buildAppBar(context, title: "用户反馈"),
         body: Column(
           children: <Widget>[
             Expanded(
@@ -133,177 +136,249 @@ class _PageState extends State<FeedbackPage> with InjectLoginApi, InjectApi {
                   builder: (context, model, child) {
                     return _isLoading
                         ? RequestLoadingWidget()
-                        : CustomScrollView(reverse: (_data.data?.length ?? 0) > 2, slivers: [
-                            SliverList(
-                              delegate: SliverChildListDelegate(
-                                _data.data
-                                    .map((e) => e.from == "-3"
-                                        ? Center(
-                                            child: Padding(
-                                                padding: EdgeInsets.symmetric(vertical: 8.0),
-                                                child: Text(
-                                                  "${DateFormat.formatTime(e.createTime)}",
-                                                  style: Theme.of(context).textTheme.bodyText1,
-                                                )),
-                                          )
-                                        : e.from == "-1"
-                                            ? Padding(
-                                                padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
-                                                child: Row(
-                                                  crossAxisAlignment: CrossAxisAlignment.start,
-                                                  children: <Widget>[
-                                                    CircleAvatar(
-                                                      backgroundImage: CachedNetworkImageProvider(
-                                                          "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2661558467,1288211245&fm=26&gp=0.jpg"),
-                                                      radius: 20,
-                                                    ),
-                                                    Space(
-                                                      width: 12,
-                                                    ),
-                                                    CustomPaint(
-                                                      painter: _BubblePainter(),
-                                                      child: Padding(
-                                                        padding: const EdgeInsets.fromLTRB(20.0, 8.0, 12, 8),
-                                                        child: Container(
-                                                          child: Text(
-                                                            "很高兴认识你",
-                                                            style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 16),
-                                                          ),
-                                                        ),
-                                                      ),
-                                                    )
-                                                  ],
-                                                ),
+                        : CustomScrollView(
+                            reverse: (_data.data?.length ?? 0) > 2,
+                            slivers: [
+                                SliverList(
+                                  delegate: SliverChildListDelegate(
+                                    _data.data
+                                        .map((e) => e.from == "-3"
+                                            ? Center(
+                                                child: Padding(
+                                                    padding:
+                                                        EdgeInsets.symmetric(
+                                                            vertical: 8.0),
+                                                    child: Text(
+                                                      "${DateFormat.formatTime(e.createTime)}",
+                                                      style: Theme.of(context)
+                                                          .textTheme
+                                                          .bodyText1,
+                                                    )),
                                               )
-                                            : e.from == "-2"
+                                            : e.from == "-1"
                                                 ? Padding(
-                                                    padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
+                                                    padding: const EdgeInsets
+                                                            .symmetric(
+                                                        vertical: 8.0,
+                                                        horizontal: 12.0),
                                                     child: Row(
-                                                      crossAxisAlignment: CrossAxisAlignment.start,
+                                                      crossAxisAlignment:
+                                                          CrossAxisAlignment
+                                                              .start,
                                                       children: <Widget>[
                                                         CircleAvatar(
-                                                          backgroundImage: CachedNetworkImageProvider(
-                                                              "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2661558467,1288211245&fm=26&gp=0.jpg"),
+                                                          backgroundImage:
+                                                              CachedNetworkImageProvider(
+                                                                  "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2661558467,1288211245&fm=26&gp=0.jpg"),
                                                           radius: 20,
                                                         ),
                                                         Space(
                                                           width: 12,
                                                         ),
                                                         CustomPaint(
-                                                          painter: _BubblePainter(),
+                                                          painter:
+                                                              _BubblePainter(),
                                                           child: Padding(
-                                                            padding: const EdgeInsets.fromLTRB(20.0, 8.0, 12, 8),
+                                                            padding:
+                                                                const EdgeInsets
+                                                                        .fromLTRB(
+                                                                    20.0,
+                                                                    8.0,
+                                                                    12,
+                                                                    8),
                                                             child: Container(
-                                                                child: Column(
-                                                              crossAxisAlignment: CrossAxisAlignment.start,
-                                                              children: <Widget>[
-                                                                Text(
-                                                                  "常见问题",
-                                                                  style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 16),
-                                                                ),
-                                                                Column(
-                                                                  crossAxisAlignment: CrossAxisAlignment.start,
-                                                                  children: model.data.data
-                                                                      .map((e) => InkWell(
-                                                                          onTap: () {
-                                                                            NavigatorUtil.goPage(context, (context) => FeedbackDetailPage(e));
-                                                                            // NavigatorUtil.go(context, "${Routes.feedbackDetail}?data=${Uri.encodeComponent(json.encode(e))}");
-                                                                          },
-                                                                          child: Padding(
-                                                                            padding: EdgeInsets.symmetric(vertical: 5.0),
-                                                                            child: Text("${model.data.data.indexOf(e) + 1}.${e.groupName}",
-                                                                                style: Theme.of(context)
-                                                                                    .textTheme
-                                                                                    .subtitle1
-                                                                                    .copyWith(color: Color(0xff666666), fontSize: 16)),
-                                                                          )))
-                                                                      .toList(),
-                                                                ),
-                                                              ],
-                                                            )),
+                                                              child: Text(
+                                                                "很高兴认识你",
+                                                                style: Theme.of(
+                                                                        context)
+                                                                    .textTheme
+                                                                    .subtitle1
+                                                                    .copyWith(
+                                                                        fontSize:
+                                                                            16),
+                                                              ),
+                                                            ),
                                                           ),
-                                                        ),
+                                                        )
                                                       ],
                                                     ),
                                                   )
-                                                : Padding(
-                                                    padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
-                                                    child: Row(
-                                                      crossAxisAlignment: CrossAxisAlignment.start,
-                                                      mainAxisAlignment: e.from == "user" ? MainAxisAlignment.end : MainAxisAlignment.start,
-                                                      children: e.from == "user"
-                                                          ? <Widget>[
-                                                              CustomPaint(
-                                                                  painter: _BubblePainterRight(),
-                                                                  child: ConstrainedBox(
-                                                                    constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
-                                                                    child: Container(
-                                                                      padding: EdgeInsets.fromLTRB(12, 6, 20, 8),
-                                                                      child: Column(
-                                                                        children: <Widget>[
-                                                                          Text(
-                                                                            "${e.content}",
-                                                                            style: Theme.of(context)
-                                                                                .textTheme
-                                                                                .subtitle1
-                                                                                .copyWith(fontSize: 16, color: Colors.white),
-                                                                          ),
-                                                                          if (e.images?.isNotEmpty == true)
-                                                                            GridView.count(
-                                                                                physics: new NeverScrollableScrollPhysics(),
-                                                                                shrinkWrap: true,
-                                                                                padding: EdgeInsets.zero,
-                                                                                crossAxisSpacing: 10.0,
-                                                                                crossAxisCount: e.images?.length ?? 0,
-                                                                                children: e.images
-                                                                                    .asMap()
-                                                                                    .keys
-                                                                                    .map((i) => CachedNetworkImage(
-                                                                                          imageUrl: e.images[i],
-                                                                                          fit: BoxFit.cover,
-                                                                                        ))
-                                                                                    .toList())
-                                                                        ],
-                                                                        crossAxisAlignment: CrossAxisAlignment.start,
-                                                                      ),
+                                                : e.from == "-2"
+                                                    ? Padding(
+                                                        padding:
+                                                            const EdgeInsets
+                                                                    .symmetric(
+                                                                vertical: 8.0,
+                                                                horizontal:
+                                                                    12.0),
+                                                        child: Row(
+                                                          crossAxisAlignment:
+                                                              CrossAxisAlignment
+                                                                  .start,
+                                                          children: <Widget>[
+                                                            CircleAvatar(
+                                                              backgroundImage:
+                                                                  CachedNetworkImageProvider(
+                                                                      "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2661558467,1288211245&fm=26&gp=0.jpg"),
+                                                              radius: 20,
+                                                            ),
+                                                            Space(
+                                                              width: 12,
+                                                            ),
+                                                            CustomPaint(
+                                                              painter:
+                                                                  _BubblePainter(),
+                                                              child: Padding(
+                                                                padding:
+                                                                    const EdgeInsets
+                                                                            .fromLTRB(
+                                                                        20.0,
+                                                                        8.0,
+                                                                        12,
+                                                                        8),
+                                                                child: Container(
+                                                                    child: Column(
+                                                                  crossAxisAlignment:
+                                                                      CrossAxisAlignment
+                                                                          .start,
+                                                                  children: <
+                                                                      Widget>[
+                                                                    Text(
+                                                                      "常见问题",
+                                                                      style: Theme.of(
+                                                                              context)
+                                                                          .textTheme
+                                                                          .subtitle1
+                                                                          .copyWith(
+                                                                              fontSize: 16),
                                                                     ),
-                                                                  )),
-                                                              Space(
-                                                                width: 12,
-                                                              ),
-                                                              CircleAvatar(
-                                                                backgroundImage: userAvatarProvider(_user?.avatar),
-                                                                radius: 20,
-                                                              ),
-                                                            ]
-                                                          : <Widget>[
-                                                              CircleAvatar(
-                                                                backgroundImage: CachedNetworkImageProvider(
-                                                                    "https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2661558467,1288211245&fm=26&gp=0.jpg"),
-                                                                radius: 20,
-                                                              ),
-                                                              Space(
-                                                                width: 12,
+                                                                    Column(
+                                                                      crossAxisAlignment:
+                                                                          CrossAxisAlignment
+                                                                              .start,
+                                                                      children: model
+                                                                          .data
+                                                                          .data
+                                                                          .map((e) => InkWell(
+                                                                              onTap: () {
+                                                                                NavigatorUtil.goPage(context, (context) => FeedbackDetailPage(e));
+                                                                                // NavigatorUtil.go(context, "${Routes.feedbackDetail}?data=${Uri.encodeComponent(json.encode(e))}");
+                                                                              },
+                                                                              child: Padding(
+                                                                                padding: EdgeInsets.symmetric(vertical: 5.0),
+                                                                                child: Text("${model.data.data.indexOf(e) + 1}.${e.groupName}", style: Theme.of(context).textTheme.subtitle1.copyWith(color: Color(0xff666666), fontSize: 16)),
+                                                                              )))
+                                                                          .toList(),
+                                                                    ),
+                                                                  ],
+                                                                )),
                                                               ),
-                                                              CustomPaint(
-                                                                  painter: _BubblePainter(),
-                                                                  child: ConstrainedBox(
-                                                                    constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
-                                                                    child: Container(
-                                                                      padding: EdgeInsets.fromLTRB(12, 6, 20, 8),
-                                                                      child: Text(
-                                                                        "${e.content}",
-                                                                        style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 16),
+                                                            ),
+                                                          ],
+                                                        ),
+                                                      )
+                                                    : Padding(
+                                                        padding:
+                                                            const EdgeInsets
+                                                                    .symmetric(
+                                                                vertical: 8.0,
+                                                                horizontal:
+                                                                    12.0),
+                                                        child: Row(
+                                                          crossAxisAlignment:
+                                                              CrossAxisAlignment
+                                                                  .start,
+                                                          mainAxisAlignment: e
+                                                                      .from ==
+                                                                  "user"
+                                                              ? MainAxisAlignment
+                                                                  .end
+                                                              : MainAxisAlignment
+                                                                  .start,
+                                                          children:
+                                                              e.from == "user"
+                                                                  ? <Widget>[
+                                                                      CustomPaint(
+                                                                          painter:
+                                                                              _BubblePainterRight(),
+                                                                          child:
+                                                                              ConstrainedBox(
+                                                                            constraints:
+                                                                                BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
+                                                                            child:
+                                                                                Container(
+                                                                              padding: EdgeInsets.fromLTRB(12, 6, 20, 8),
+                                                                              child: Column(
+                                                                                children: <Widget>[
+                                                                                  Text(
+                                                                                    "${e.content}",
+                                                                                    style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 16, color: Color(0xff333333)),
+                                                                                  ),
+                                                                                  if (e.images?.isNotEmpty == true)
+                                                                                    GridView.count(
+                                                                                        physics: new NeverScrollableScrollPhysics(),
+                                                                                        shrinkWrap: true,
+                                                                                        padding: EdgeInsets.zero,
+                                                                                        crossAxisSpacing: 10.0,
+                                                                                        crossAxisCount: e.images?.length ?? 0,
+                                                                                        children: e.images
+                                                                                            .asMap()
+                                                                                            .keys
+                                                                                            .map((i) => CachedNetworkImage(
+                                                                                                  imageUrl: e.images[i],
+                                                                                                  fit: BoxFit.cover,
+                                                                                                ))
+                                                                                            .toList())
+                                                                                ],
+                                                                                crossAxisAlignment: CrossAxisAlignment.start,
+                                                                              ),
+                                                                            ),
+                                                                          )),
+                                                                      Space(
+                                                                        width:
+                                                                            12,
                                                                       ),
-                                                                    ),
-                                                                  )),
-                                                            ],
-                                                    ),
-                                                  ))
-                                    .toList(),
-                              ),
-                            )
-                          ]);
+                                                                      CircleAvatar(
+                                                                        backgroundImage:
+                                                                            userAvatarProvider(_user?.avatar),
+                                                                        radius:
+                                                                            20,
+                                                                      ),
+                                                                    ]
+                                                                  : <Widget>[
+                                                                      CircleAvatar(
+                                                                        backgroundImage:
+                                                                            CachedNetworkImageProvider("https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2661558467,1288211245&fm=26&gp=0.jpg"),
+                                                                        radius:
+                                                                            20,
+                                                                      ),
+                                                                      Space(
+                                                                        width:
+                                                                            12,
+                                                                      ),
+                                                                      CustomPaint(
+                                                                          painter:
+                                                                              _BubblePainter(),
+                                                                          child:
+                                                                              ConstrainedBox(
+                                                                            constraints:
+                                                                                BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.6),
+                                                                            child:
+                                                                                Container(
+                                                                              padding: EdgeInsets.fromLTRB(12, 6, 20, 8),
+                                                                              child: Text(
+                                                                                "${e.content}",
+                                                                                style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 16),
+                                                                              ),
+                                                                            ),
+                                                                          )),
+                                                                    ],
+                                                        ),
+                                                      ))
+                                        .toList(),
+                                  ),
+                                )
+                              ]);
                   },
                 ),
               ),
@@ -319,8 +394,10 @@ class _PageState extends State<FeedbackPage> with InjectLoginApi, InjectApi {
                         _postImage();
                       },
                       child: Padding(
-                        padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
-                        child: Image.asset("lib/assets/img/bbs_icon_picture.png"),
+                        padding: const EdgeInsets.symmetric(
+                            vertical: 8.0, horizontal: 12.0),
+                        child:
+                            Image.asset("lib/assets/img/bbs_icon_picture.png"),
                       ),
                     ),
                     Expanded(
@@ -329,16 +406,25 @@ class _PageState extends State<FeedbackPage> with InjectLoginApi, InjectApi {
                           controller: _controller,
                           focusNode: _focusNode,
                           keyboardType: TextInputType.multiline,
+                          strutStyle:
+                              StrutStyle(forceStrutHeight: true, height: 1.4),
                           minLines: 1,
                           maxLines: 3,
                           maxLength: 200,
+                          style: TextStyle(fontSize: 16.0),
                           onChanged: (value) {
-                             setState(() {
+                            setState(() {
                               _textFieldValue = value;
-                             });
+                            });
                           },
                           decoration: InputDecoration(
-                              counterText: "", hintText: "提交你的反馈...", contentPadding: EdgeInsets.symmetric(vertical: 16.0), border: InputBorder.none),
+                              counterText: "",
+                              hintText: "提交你的反馈...",
+                              contentPadding:
+                                  EdgeInsets.symmetric(vertical: 16.0),
+                              border: InputBorder.none,
+                            hintStyle: TextStyle(color: Color(0xff999999))
+                          ),
                         ),
                       ),
                     ),
@@ -350,18 +436,21 @@ class _PageState extends State<FeedbackPage> with InjectLoginApi, InjectApi {
                               width: 22,
                               child: CircularProgressIndicator(),
                             )
-                          : PrimaryButton(
-                              width: 80,
-                              height: 35,
-                              callback: _textFieldValue.isEmpty
-                                  ? null
-                                  : () async {
+                          :
+                          ValueListenableBuilder(
+                              valueListenable: _postable,
+                              builder: (_, able, __) => PrimaryButton(
+                                    content: "提交",
+                                    width: 80,
+                                    height: 35,
+                                    callback: () async {
                                       setState(() {
                                         _posting = true;
                                       });
                                       _controller.clear();
 
-                                      await _postFeedBackpostFeedBack(_textFieldValue);
+                                      await _postFeedBackpostFeedBack(
+                                          _textFieldValue);
                                       // ToastUtil.show("提交成功");
 
                                       setState(() {
@@ -369,10 +458,12 @@ class _PageState extends State<FeedbackPage> with InjectLoginApi, InjectApi {
                                         _posting = false;
                                       });
                                       refresh();
-//                      SystemChannels.textInput.invokeMethod('TextInput.hide');
                                     },
-                              content: "提交",
-                            ),
+                                    shadow: able == true,
+                                    buttonColor: able == false
+                                        ? Color(0xffFFC400).withOpacity(0.3)
+                                        : null,
+                                  )),
                     )
                   ],
                 ),
@@ -417,7 +508,8 @@ class _PageState extends State<FeedbackPage> with InjectLoginApi, InjectApi {
 
   _postFeedBackpostFeedBack(String content, {int typeId = 0}) async {
     typeId == null ? typeId = 0 : typeId = typeId;
-    await api.postFeedback(typeId, content, extra: await Application.getDeviceInfo());
+    await api.postFeedback(typeId, content,
+        extra: await Application.getDeviceInfo());
   }
 }
 
@@ -464,7 +556,8 @@ class PostActionState extends State<PostAction> with InjectApi {
         Asset asset = imageList[i];
         if (upload.containsKey(asset)) continue;
         ByteData byteData = await asset.getByteData(quality: 85);
-        File file = File('${directory.path}/${DateTime.now().millisecondsSinceEpoch}_$i.jpg');
+        File file = File(
+            '${directory.path}/${DateTime.now().millisecondsSinceEpoch}_$i.jpg');
         List<int> bytes = byteData.buffer.asUint8List().toList();
         print('临时文件 ${file.path} ${bytes.length}');
         file.writeAsBytesSync(bytes);
@@ -479,7 +572,9 @@ class PostActionState extends State<PostAction> with InjectApi {
 
     _msg.value = "发布中...";
     // await Future.delayed(Duration(seconds: 3));
-    var data = await api.postFeedback(0, '图片', images: json.encode(upload.values.toList()), extra: await Application.getDeviceInfo());
+    var data = await api.postFeedback(0, '图片',
+        images: json.encode(upload.values.toList()),
+        extra: await Application.getDeviceInfo());
     print(data);
     if (data.code == 0) {
       Navigator.of(context).pop(true);
@@ -497,7 +592,10 @@ class PostActionState extends State<PostAction> with InjectApi {
           CircularProgressIndicator(),
           Padding(
             padding: const EdgeInsets.only(top: 15),
-            child: ValueListenableBuilder(valueListenable: _msg, builder: (BuildContext context, String value, Widget child) => Text(value)),
+            child: ValueListenableBuilder(
+                valueListenable: _msg,
+                builder: (BuildContext context, String value, Widget child) =>
+                    Text(value)),
           )
         ],
       ),
@@ -505,6 +603,55 @@ class PostActionState extends State<PostAction> with InjectApi {
   }
 }
 
+//class _BubblePainter extends CustomPainter {
+//  final circular = Radius.circular(10);
+//  final Paint _paint = Paint()..color = Colors.white;
+//  final double _bubbleWidth = 10;
+//
+//  @override
+//  void paint(Canvas canvas, Size size) {
+////    Path path = Path()..moveTo(size.width,  size.height / 2)..quadraticBezierTo(size.width / 3*2, size.height / 2 + 50, 0, size.height / 2)
+////    ..quadraticBezierTo(size.width / 3, size.height,size.width,size.height)..close();
+//    Path path = Path()
+//      ..moveTo(_bubbleWidth + 1, _bubbleWidth)
+//      ..quadraticBezierTo(_bubbleWidth / 3 * 2, _bubbleWidth + 4, 0, _bubbleWidth)
+//      ..quadraticBezierTo(_bubbleWidth / 3, _bubbleWidth + _bubbleWidth / 2, _bubbleWidth + 1, _bubbleWidth * 2)
+//      ..close();
+//    canvas.drawPath(path, _paint);
+//    canvas.drawRRect(RRect.fromLTRBR(_bubbleWidth, 0, size.width, size.height, circular), _paint);
+//  }
+//
+//  @override
+//  bool shouldRepaint(CustomPainter oldDelegate) {
+//    return oldDelegate != this;
+//  }
+//}
+//
+//class _BubblePainterRight extends CustomPainter {
+//  final circular = Radius.circular(10);
+//  final Paint _paint = Paint()..color = Color(0xffFFC400);
+//  final double _bubbleWidth = 10;
+//
+//  @override
+//  void paint(Canvas canvas, Size size) {
+////    Path path = Path()..moveTo(size.width,  size.height / 2)..quadraticBezierTo(size.width / 3*2, size.height / 2 + 50, 0, size.height / 2)
+////    ..quadraticBezierTo(size.width / 3, size.height,size.width,size.height)..close();
+//    double left = size.width - _bubbleWidth;
+//    Path path = Path()
+//      ..moveTo(left, _bubbleWidth)
+//      ..quadraticBezierTo(left + _bubbleWidth / 3 * 2, _bubbleWidth + 4, size.width, _bubbleWidth)
+//      ..quadraticBezierTo(left + _bubbleWidth / 3, _bubbleWidth + _bubbleWidth / 2, left, _bubbleWidth * 2)
+//      ..close();
+//    canvas.drawPath(path, _paint);
+//    canvas.drawRRect(RRect.fromLTRBR(0, 0, size.width - _bubbleWidth, size.height, circular), _paint);
+//  }
+//
+//  @override
+//  bool shouldRepaint(CustomPainter oldDelegate) {
+//    return oldDelegate != this;
+//  }
+//}
+
 class _BubblePainter extends CustomPainter {
   final circular = Radius.circular(10);
   final Paint _paint = Paint()..color = Colors.white;
@@ -516,11 +663,15 @@ class _BubblePainter extends CustomPainter {
 //    ..quadraticBezierTo(size.width / 3, size.height,size.width,size.height)..close();
     Path path = Path()
       ..moveTo(_bubbleWidth + 1, _bubbleWidth)
-      ..quadraticBezierTo(_bubbleWidth / 3 * 2, _bubbleWidth + 4, 0, _bubbleWidth)
-      ..quadraticBezierTo(_bubbleWidth / 3, _bubbleWidth + _bubbleWidth / 2, _bubbleWidth + 1, _bubbleWidth * 2)
+      ..quadraticBezierTo(
+          _bubbleWidth / 3 * 2, _bubbleWidth + 4, 0, _bubbleWidth)
+      ..quadraticBezierTo(_bubbleWidth / 3, _bubbleWidth + _bubbleWidth / 2,
+          _bubbleWidth + 1, _bubbleWidth * 2)
       ..close();
     canvas.drawPath(path, _paint);
-    canvas.drawRRect(RRect.fromLTRBR(_bubbleWidth, 0, size.width, size.height, circular), _paint);
+    canvas.drawRRect(
+        RRect.fromLTRBR(_bubbleWidth, 0, size.width, size.height, circular),
+        _paint);
   }
 
   @override
@@ -531,7 +682,7 @@ class _BubblePainter extends CustomPainter {
 
 class _BubblePainterRight extends CustomPainter {
   final circular = Radius.circular(10);
-  final Paint _paint = Paint()..color = Color(0xffFFC400);
+  final Paint _paint = Paint()..color = Color(0xffffe400).withOpacity(0.7);
   final double _bubbleWidth = 10;
 
   @override
@@ -541,11 +692,15 @@ class _BubblePainterRight extends CustomPainter {
     double left = size.width - _bubbleWidth;
     Path path = Path()
       ..moveTo(left, _bubbleWidth)
-      ..quadraticBezierTo(left + _bubbleWidth / 3 * 2, _bubbleWidth + 4, size.width, _bubbleWidth)
-      ..quadraticBezierTo(left + _bubbleWidth / 3, _bubbleWidth + _bubbleWidth / 2, left, _bubbleWidth * 2)
+      ..quadraticBezierTo(left + _bubbleWidth / 3 * 2, _bubbleWidth + 2,
+          size.width, _bubbleWidth)
+      ..quadraticBezierTo(left + _bubbleWidth / 3,
+          _bubbleWidth + _bubbleWidth / 2, left, _bubbleWidth * 2)
       ..close();
     canvas.drawPath(path, _paint);
-    canvas.drawRRect(RRect.fromLTRBR(0, 0, size.width - _bubbleWidth, size.height, circular), _paint);
+    canvas.drawRRect(
+        RRect.fromLTRBR(0, 0, size.width - _bubbleWidth, size.height, circular),
+        _paint);
   }
 
   @override

+ 14 - 1
lib/pages/my/game_list_page.dart

@@ -123,7 +123,20 @@ class _GameListPageState extends State<GameListPage> with InjectApi, WidgetsBind
           children: <Widget>[
             Row(
               children: <Widget>[
-                CircleAvatar(backgroundImage: CachedNetworkImageProvider(data.cover), radius: 35.0),
+//                CircleAvatar(backgroundImage: CachedNetworkImageProvider(data.cover), radius: 35.0),
+                Container(
+                  width: 70.0,
+                  height: 70.0,
+//                  margin: EdgeInsets.only(right: 12.0),
+                  child: ClipRRect(
+                    child: CachedNetworkImage(
+                      imageUrl: data.cover,
+                      fit: BoxFit.cover,
+                    ),
+                    // 也可控件一边圆角大小
+                    borderRadius: new BorderRadius.all(Radius.circular(6.0)),
+                  ),
+                ),
                 Expanded(
                   child: Padding(
                     padding: const EdgeInsets.symmetric(horizontal: 12.0),

+ 1 - 1
lib/pages/my/level_page.dart

@@ -185,7 +185,7 @@ class _PageState extends State<LevelPage> with InjectLoginApi {
                                         imageUrl: _data.level.logo ?? "",fit: BoxFit.contain,))),
                             Text(
                               "Lv.${_data.level.level}",
-                              style: Theme.of(context).textTheme.headline3,
+                              style: Theme.of(context).textTheme.headline3.copyWith(fontSize: 20.0),
                             ),
                             Space(
                               height: 24,

+ 1 - 8
lib/pages/social/block_user_list_page.dart

@@ -42,14 +42,7 @@ class _PageDetailState extends ViewStateLifecycle<BlockUserListPage, SimpleModel
   Widget build(BuildContext context) {
     return Scaffold(
       backgroundColor: Colors.white,
-      appBar: AppBar(
-        elevation: 0,
-        leading: buildBackButton(context),
-        title: Text(
-          "屏蔽列表",
-          style: titleStyle,
-        ),
-      ),
+      appBar: buildAppBar(context, title: "屏蔽列表"),
       body: ProviderWidget<SimpleModel>(
           model: model,
           onModelReady: (model) => model.initData(),

+ 74 - 24
lib/pages/social/new_social_index_page.dart

@@ -1,6 +1,6 @@
+import 'dart:math';
 import 'dart:ui';
 
-import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/rendering.dart';
@@ -10,7 +10,6 @@ import 'package:provider/provider.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 import 'package:sport/bean/forum.dart';
 import 'package:sport/bean/post.dart';
-import 'package:sport/pages/social/post_detail_page.dart';
 import 'package:sport/pages/social/post_page.dart';
 import 'package:sport/pages/social/post_widget.dart';
 import 'package:sport/pages/social/search_page.dart';
@@ -74,7 +73,7 @@ class _PageState extends ViewStateLifecycle<NewSocialIndexPage, SocialDetailMode
   ];
   ValueNotifier<bool> isShowSelect = ValueNotifier(false);
 
-  void _refresh(){
+  void _refresh() {
     model?.setForumIdAndOrigin(_tabController.index, forumId, isOfficial);
     _controller.animateTo(0, duration: Duration(milliseconds: 100), curve: Curves.ease);
   }
@@ -105,7 +104,6 @@ class _PageState extends ViewStateLifecycle<NewSocialIndexPage, SocialDetailMode
         }
       });
     initButtonList();
-
   }
 
   @override
@@ -177,7 +175,7 @@ class _PageState extends ViewStateLifecycle<NewSocialIndexPage, SocialDetailMode
             color: index == targetIndex.value ? Theme.of(context).accentColor : Colors.white,
             borderRadius: BorderRadius.all(Radius.circular(20.0)),
             border: Border.all(color: index == targetIndex.value ? Colors.white : Theme.of(context).dividerTheme.color)),
-        padding: EdgeInsets.symmetric(vertical: 8.0,horizontal: 25.0),
+        padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 25.0),
         child: Text(
           data.gameName,
           strutStyle: StrutStyle(forceStrutHeight: true),
@@ -348,6 +346,8 @@ class _PageState extends ViewStateLifecycle<NewSocialIndexPage, SocialDetailMode
                           buildSliverAppBar(context, "社区",
                               canBack: false,
                               pinned: false,
+                              height: 100.0,
+                              padding: const EdgeInsets.fromLTRB(12.0, 0, 0, 6.0),
                               actions: <Widget>[
                                 Selector<SocialIndexModel, ViewState>(
                                     selector: (_, SocialIndexModel model) => model.viewState,
@@ -399,11 +399,11 @@ class _PageState extends ViewStateLifecycle<NewSocialIndexPage, SocialDetailMode
                           header,
                           SliverPersistentHeader(
                             delegate: PersistentHeader(
-                                min: 34,
-                                max: 34,
+                                min: 50,
+                                max: 50,
                                 child: Container(
                                   color: Colors.white,
-                                  padding: EdgeInsets.only(bottom: 5),
+                                  padding: EdgeInsets.only(bottom: 10),
                                   child: TabBar(
                                     isScrollable: true,
                                     indicatorPadding: EdgeInsets.symmetric(horizontal: 8),
@@ -416,7 +416,7 @@ class _PageState extends ViewStateLifecycle<NewSocialIndexPage, SocialDetailMode
                           ),
                           SliverToBoxAdapter(
                             child: Padding(
-                              padding: EdgeInsets.symmetric(vertical: 12.0, horizontal: 12.0),
+                              padding: EdgeInsets.symmetric(vertical: 6.0, horizontal: 12.0),
                               child: Row(
                                 children: <Widget>[
                                   _buildSearchWidget(),
@@ -490,19 +490,10 @@ class _PageState extends ViewStateLifecycle<NewSocialIndexPage, SocialDetailMode
               });
         },
       ),
+      floatingActionButtonLocation: const _EndFloatFloatingActionButtonLocation(),
+      floatingActionButtonAnimator:const _ScalingFabMotionAnimator(),
       floatingActionButton: InkWell(
-        child: Container(
-          width: 88.0,
-          height: 88.0,
-          alignment: Alignment.centerRight,
-          decoration: BoxDecoration(
-            image: DecorationImage(
-                image: AssetImage(
-              "lib/assets/img/bbs_icon_edit.png",
-            )),
-//            color: Colors.white,
-          ),
-        ),
+        child: Image.asset("lib/assets/img/bbs_icon_edit.png"),
         onTap: () async {
 //            print('FloatingActionButton');
           var result = await NavigatorUtil.goPage(
@@ -511,16 +502,75 @@ class _PageState extends ViewStateLifecycle<NewSocialIndexPage, SocialDetailMode
                     "",
                     forums: buttonList,
                   ));
-          if(result == true){
+          if (result == true) {
             if (_tabController.index.toDouble() == _tabController.animation.value) {
               _refresh();
-            }else{
+            } else {
               _tabController?.index = 2;
             }
-
           }
         },
       ),
     );
   }
 }
+
+class _EndFloatFloatingActionButtonLocation extends FloatingActionButtonLocation {
+  const _EndFloatFloatingActionButtonLocation();
+
+  double _rightOffset(ScaffoldPrelayoutGeometry scaffoldGeometry, {double offset = 0.0}) {
+    return scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.minInsets.right - scaffoldGeometry.floatingActionButtonSize.width + offset;
+  }
+
+  double getDockedY(ScaffoldPrelayoutGeometry scaffoldGeometry) {
+    final double contentBottom = scaffoldGeometry.contentBottom;
+    final double bottomSheetHeight = scaffoldGeometry.bottomSheetSize.height;
+    final double fabHeight = scaffoldGeometry.floatingActionButtonSize.height;
+    final double snackBarHeight = scaffoldGeometry.snackBarSize.height;
+
+    double fabY = contentBottom - fabHeight / 2.0;
+    // The FAB should sit with a margin between it and the snack bar.
+    if (snackBarHeight > 0.0) fabY = min(fabY, contentBottom - snackBarHeight - fabHeight - kFloatingActionButtonMargin);
+    // The FAB should sit with its center in front of the top of the bottom sheet.
+    if (bottomSheetHeight > 0.0) fabY = min(fabY, contentBottom - bottomSheetHeight - fabHeight / 2.0);
+
+    final double maxFabY = scaffoldGeometry.scaffoldSize.height - fabHeight;
+    return min(maxFabY, fabY);
+  }
+
+  @override
+  Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) {
+    final double fabX = _rightOffset(scaffoldGeometry, offset: -8.0);
+    final double fabY = getDockedY(scaffoldGeometry);
+    return Offset(fabX, fabY - 8);
+  }
+
+  @override
+  String toString() => 'FloatingActionButtonLocation.endFloat';
+}
+
+class _ScalingFabMotionAnimator extends FloatingActionButtonAnimator {
+  const _ScalingFabMotionAnimator();
+
+  @override
+  Offset getOffset({ Offset begin, Offset end, double progress }) {
+
+    return end;
+  }
+
+  @override
+  Animation<double> getScaleAnimation({ Animation<double> parent }) {
+    return AlwaysStoppedAnimation(1.0);
+  }
+
+  @override
+  Animation<double> getRotationAnimation({ Animation<double> parent }) {
+    return AlwaysStoppedAnimation(1.0);
+  }
+
+  // If the animation was just starting, we'll continue from where we left off.
+  // If the animation was finishing, we'll treat it as if we were starting at that point in reverse.
+  // This avoids a size jump during the animation.
+  @override
+  double getAnimationRestart(double previousValue) => 1.0;
+}

+ 1 - 11
lib/pages/social/post_detail_page.dart

@@ -593,17 +593,7 @@ class _PageState extends ViewStateLifecycle<PostDetailPage, PostDetailModel> wit
   Widget build(BuildContext context) {
     return Scaffold(
       backgroundColor: Colors.white,
-      appBar: AppBar(
-        titleSpacing: -5,
-        title: Container(
-          alignment: Alignment.centerLeft,
-          child: Text(
-            "帖子详情",
-            style: titleStyle,
-          ),
-        ),
-        leading: buildBackButton(context),
-        actions: <Widget>[
+      appBar: buildAppBar(context, title: "帖子详情" , actions: <Widget>[
       PopupMenuTheme(
       data: PopupMenuThemeData(shape: PopmenuShape(borderRadius: BorderRadius.all(Radius.circular(10.0)))),
     child: PopupMenuButton(

+ 1 - 1
lib/pages/social/post_widget.dart

@@ -431,7 +431,7 @@ class _PostWidgetState extends State<PostWidget> with InjectApi {
                     crossAxisAlignment: CrossAxisAlignment.start,
                     children: <Widget>[
                       Padding(
-                        padding: const EdgeInsets.only(top: 12.0),
+                        padding: const EdgeInsets.only(top: 6.0),
                         child: Column(
                           crossAxisAlignment: CrossAxisAlignment.start,
                           children: <Widget>[

+ 27 - 22
lib/pages/social/search_page.dart

@@ -119,32 +119,37 @@ class _PageState extends State<SearchPage> with UserId, InjectApi {
                     width: 12,
                   ),
                   Image.asset("lib/assets/img/searchbar_icon_search.png"),
+                  Space(
+                    width: 6,
+                  ),
                   Expanded(
                       child: Selector<SearchModel, String>(
                     selector: (_, model) => model.searchValue,
-                    builder: (context, _value, child) => TextField(
-                      controller: _controller,
-                      maxLines: 1,
-                      focusNode: _focusNode,
-                      decoration: InputDecoration(
-                          hintText: '请输入搜索内容',
-                          contentPadding: EdgeInsets.symmetric(
-                              vertical: 11.5, horizontal: 6.0),
-                          border: InputBorder.none,
-                          hintStyle: TextStyle(color: Color(0xff999999))),
-                      onChanged: debounceValueChanged((value) {
-                        if(value != ""){
+                    builder: (context, _value, child) => Container(
+                      constraints: BoxConstraints(maxHeight: 30),
+                      child: TextField(
+                        controller: _controller,
+                        maxLines: 1,
+                        focusNode: _focusNode,
+                        decoration: InputDecoration(
+                            hintText: '请输入搜索内容',
+                            contentPadding: const EdgeInsets.symmetric(vertical: 4.0),
+                            border: OutlineInputBorder(borderSide: BorderSide.none),
+                            hintStyle: TextStyle(color: Color(0xff999999))),
+                        onChanged: debounceValueChanged((value) {
+                          if(value != ""){
+                            _submitValue(value);
+                          }
+                          _model.queryValue(value);
+                          Provider.of<SearchModel>(context, listen: false)
+                              .updateSearchValue(value);
+                        }),
+                        onSubmitted: (value) {
                           _submitValue(value);
-                        }
-                        _model.queryValue(value);
-                        Provider.of<SearchModel>(context, listen: false)
-                            .updateSearchValue(value);
-                      }),
-                      onSubmitted: (value) {
-                        _submitValue(value);
-                        // _searchModel.setKeyword(value);
-                        // Provider.of<SearchModel>(context, listen: false).queryValue(value);
-                      },
+                          // _searchModel.setKeyword(value);
+                          // Provider.of<SearchModel>(context, listen: false).queryValue(value);
+                        },
+                      ),
                     ),
                   )),
                   Visibility(

+ 1 - 11
lib/pages/social/share_achievement.dart

@@ -22,16 +22,7 @@ class ShareAchievementPage extends StatelessWidget {
 //    print("${DateTime.now().millisecondsSinceEpoch}");
     // TODO: implement build
     return Scaffold(
-        appBar: AppBar(
-          titleSpacing: -5,
-          title: Container(
-            child: Text(
-              "成就分享",
-              style: titleStyle,
-            ),
-            alignment: Alignment.centerLeft,
-          ),
-          leading: buildBackButton(context),
+        appBar: buildAppBar(context, title: "成就分享"),
 //          actions: <Widget>[
 //            IconButton(
 //              icon: Image.asset("lib/assets/img/bbs_icon_share.png"),
@@ -41,7 +32,6 @@ class ShareAchievementPage extends StatelessWidget {
 //              },
 //            )
 //          ],
-        ),
         body: Stack(children: [
           Container(
               color: Color(0xff999999),

+ 61 - 76
lib/pages/social/user_friend_add_page.dart

@@ -1,16 +1,17 @@
 import 'dart:async';
 import 'dart:convert';
 
-import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart' hide NestedScrollView;
-import 'package:flutter_dong_scan/scan.dart';
+import 'package:flutter/services.dart';
 import 'package:flutter_easyrefresh/easy_refresh.dart';
+import 'package:qrscanner/flutter_plugin_qr_scanner.dart';
 import 'package:provider/provider.dart';
 import 'package:qr_flutter/qr_flutter.dart';
 import 'package:sport/bean/post_user.dart';
 import 'package:sport/bean/user_friend.dart';
 import 'package:sport/bean/user_info.dart';
+// import 'package:sport/pages/social/qr_view.dart';
 import 'package:sport/pages/social/user_detail_page.dart';
 import 'package:sport/provider/lib/provider_widget.dart';
 import 'package:sport/provider/lib/simple_model.dart';
@@ -18,27 +19,22 @@ import 'package:sport/provider/lib/view_state_lifecycle.dart';
 import 'package:sport/provider/user_model.dart';
 import 'package:sport/router/navigator_util.dart';
 import 'package:sport/services/api/inject_api.dart';
-import 'package:sport/utils/DateFormat.dart';
 import 'package:sport/utils/click.dart';
 import 'package:sport/utils/toast.dart';
 import 'package:sport/widgets/appbar.dart';
-import 'package:sport/widgets/button_cancel.dart';
-import 'package:sport/widgets/dialog/scan_add_new_friend_dialog.dart';
 import 'package:sport/widgets/dialog/request_dialog.dart';
+import 'package:sport/widgets/dialog/scan_add_new_friend_dialog.dart';
 import 'package:sport/widgets/error.dart';
 import 'package:sport/widgets/image.dart';
 import 'package:sport/widgets/loading.dart';
 import 'package:sport/widgets/misc.dart';
-import 'package:sport/widgets/persistent_header.dart';
-import 'package:sport/widgets/space.dart';
 
 class UserFriendAddPage extends StatefulWidget {
   @override
   State<StatefulWidget> createState() => _PageState();
 }
 
-class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel>
-    with InjectApi {
+class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel> with InjectApi {
   TextEditingController _controller;
   FocusNode _focusNode;
   ValueNotifier<String> _searchValue = ValueNotifier<String>("");
@@ -56,9 +52,7 @@ class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel>
   @override
   SimpleModel createModel() => SimpleModel((page) async {
         if (_searchValue.value == "") return [];
-        return (await api.userSearch(kw: _searchValue.value, page: page))
-            .pageResult
-            .results;
+        return (await api.userSearch(kw: _searchValue.value, page: page)).pageResult.results;
       });
 
 //  // 试试 写写 ViewModel
@@ -110,8 +104,7 @@ class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel>
         if (allUserFriends.length <= 0) {
           allUserFriends.addAll(userFriends);
         } else {
-          Iterable<int> allUserFriendsId =
-              allUserFriends.map((e) => e.socialInfo.id);
+          Iterable<int> allUserFriendsId = allUserFriends.map((e) => e.socialInfo.id);
           // 去重...
           for (UserFriend _userfriend in userFriends) {
             if (!allUserFriendsId.contains(_userfriend.socialInfo.id)) {
@@ -124,9 +117,7 @@ class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel>
     // 暂时这样解决
     timerAllFriend = Timer.periodic(Duration(seconds: 2), (timer) async {
       // 终极解决方案... 上锁 + 指针 / 用栈的话 还是会 有多次的。用指针就不会了
-      if (!isWaiting &&
-          allUserFriends.length > 0 &&
-          currentIndex < allUserFriends.length) {
+      if (!isWaiting && allUserFriends.length > 0 && currentIndex < allUserFriends.length) {
         isWaiting = true; //正在等待的意思...
         UserFriend currentUserFriend = allUserFriends[currentIndex];
         bool flag = await showDialog(
@@ -148,26 +139,27 @@ class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel>
   Widget build(BuildContext context) {
     return Scaffold(
       backgroundColor: Colors.white,
-      appBar: AppBar(
-        elevation: 0,
-        leading: buildBackButton(context),
-        titleSpacing: -5,
-        title:Container(
-          alignment: Alignment.centerLeft,
-          child:  Text(
-            "添加好友",
-            style: titleStyle,
-          ),
-        ),
-        actions: <Widget>[
+      appBar: buildAppBar(context, title: "添加好友", actions: <Widget>[
           IconButton(
             icon: Image.asset("lib/assets/img/fiends_image_scanning.png"),
-            onPressed: () {
-              ScanConfig scanConfig = ScanConfig();
-              SDScan scan =
-                  SDScan().setScanEventListener((dynamic friendData) async {
-                // 扫描之后就去这个逼的 主页 爱关注 不关注的...
-                var data = json.decode(friendData);
+            onPressed: () async {
+              String code;
+              // Platform messages may fail, so we use a try/catch PlatformException.
+              try {
+                code = await QrScanner.scan(
+                  title: "扫一扫",
+                  laserColor: Theme.of(context).accentColor, //default #ffff55ff
+                  playBeep: true, //default false
+                  promptMessage: "将二维码放入框内,即可自动扫描",
+                  errorMsg: "扫描错误",
+                  permissionDeniedText: "请在设置中打开定位权限后继续使用",
+                  messageConfirmText: "确定",
+                  messageCancelText: "取消",
+                );
+              } on PlatformException {
+                code = 'Failed to get qr code.';
+              }
+              var data = json.decode(code);
 
                 UserInfo _userInfo =
                     (await api.getUserInfo('${data['uid']}')).data;
@@ -177,8 +169,23 @@ class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel>
                   newFriendCode = data['code'];
                   _showConfirmDialog();
                 });
-              });
-              scan.startScan(config: scanConfig);
+
+              // ScanConfig scanConfig = ScanConfig();
+              // SDScan scan =
+              //     SDScan().setScanEventListener((dynamic friendData) async {
+              //   // 扫描之后就去这个逼的 主页 爱关注 不关注的...
+              //   var data = json.decode(friendData);
+              //
+              //   UserInfo _userInfo =
+              //       (await api.getUserInfo('${data['uid']}')).data;
+              //
+              //   setState(() {
+              //     _newFriendUserInfo = _userInfo;
+              //     newFriendCode = data['code'];
+              //     _showConfirmDialog();
+              //   });
+              // });
+              // scan.startScan(config: scanConfig);
             },
           ),
         ],
@@ -214,12 +221,14 @@ class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel>
                     child: RequestLoadingWidget(),
                   ),
                 if (model.isEmpty || model.isError)
-                  _searchValue.value != "" ? SliverToBoxAdapter(
-                  child: RequestErrorWidget(
-                      null,
-                      msg:  "暂无相关用户~",
-                    ),
-                  ):SliverToBoxAdapter(),
+                  _searchValue.value != ""
+                      ? SliverToBoxAdapter(
+                          child: RequestErrorWidget(
+                            null,
+                            msg: "暂无相关用户~",
+                          ),
+                        )
+                      : SliverToBoxAdapter(),
                 if (model.isIdle)
                   SliverList(
                     delegate: SliverChildBuilderDelegate(
@@ -250,8 +259,7 @@ class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel>
                                         version: QrVersions.auto,
                                         size: 170,
                                         gapless: false,
-                                        embeddedImage: userAvatarProvider(
-                                            model.user.avatar))),
+                                        embeddedImage: userAvatarProvider(model.user.avatar))),
                               )),
                   )),
               ],
@@ -297,10 +305,7 @@ class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel>
                 child: Text(
                   "已关注",
                   strutStyle: fixedLine,
-                  style: Theme.of(context)
-                      .textTheme
-                      .bodyText2
-                      .copyWith(color: Theme.of(context).accentColor),
+                  style: Theme.of(context).textTheme.bodyText2.copyWith(color: Theme.of(context).accentColor),
                 ),
               )
             : GestureDetector(
@@ -312,10 +317,7 @@ class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel>
                   child: Text(
                     "关注",
                     strutStyle: fixedLine,
-                    style: Theme.of(context)
-                        .textTheme
-                        .bodyText2
-                        .copyWith(color: Theme.of(context).accentColor),
+                    style: Theme.of(context).textTheme.bodyText2.copyWith(color: Theme.of(context).accentColor),
                   ),
                   decoration: BoxDecoration(
                     borderRadius: BorderRadius.circular(20),
@@ -328,9 +330,7 @@ class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel>
                 onTap: () async {
                   if (user.isFriend()) return;
                   await request(context, () async {
-                    var resp = await model.api
-                        .userFollow(uid: user?.id)
-                        .catchError((onError) {});
+                    var resp = await model.api.userFollow(uid: user?.id).catchError((onError) {});
                     if (resp?.code == 0) {
                       ToastUtil.show("关注成功");
                       setState(() {
@@ -348,26 +348,10 @@ class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel>
           padding: const EdgeInsets.all(12.0),
           child: InkWell(
               onTap: () async {
-                List<UserFriend> friends = model.list
-                    .map((e) => UserFriend(
-                        uid: user.id,
-                        socialInfo: user,
-                        isFriends: user.isFriend() ? "1" : "0"))
-                    .toList();
+                List<UserFriend> friends = model.list.map((e) => UserFriend(uid: user.id, socialInfo: user, isFriends: user.isFriend() ? "1" : "0")).toList();
                 await NavigatorUtil.goPage(
-                    context,
-                    (context) => UserDetailPage(
-                        PostUser(
-                            id: "${user?.id}",
-                            name: user?.name,
-                            avatar: user?.avatar),
-                        userFriends: friends));
-                user.followStatus = friends
-                            .firstWhere((element) => element.uid == user.id)
-                            ?.isFriends ==
-                        "1"
-                    ? "followed"
-                    : "none";
+                    context, (context) => UserDetailPage(PostUser(id: "${user?.id}", name: user?.name, avatar: user?.avatar), userFriends: friends));
+                user.followStatus = friends.firstWhere((element) => element.uid == user.id)?.isFriends == "1" ? "followed" : "none";
                 setState(() {});
               },
               child: child),
@@ -405,7 +389,8 @@ class _PageState extends ViewStateLifecycle<UserFriendAddPage, SimpleModel>
                 hintText: '输入账号/用户昵称',
                 border: InputBorder.none,
                 contentPadding: EdgeInsets.symmetric(
-                  vertical: 11.5,),
+                  vertical: 11.5,
+                ),
               ),
               onChanged: debounceValueChanged((value) {
                 _searchValue.value = value;

+ 3 - 12
lib/pages/social/user_friend_page.dart

@@ -236,18 +236,9 @@ class _PageDetailState
                                     children: <Widget>[
                                       ListTile(
                                         leading: InkWell(
-                                          child: Container(
-                                            width: 44,
-                                            height: 44,
-                                            decoration: BoxDecoration(
-                                              shape: BoxShape.rectangle,
-                                              borderRadius:
-                                              BorderRadius.circular(4.0),
-                                              image: DecorationImage(
-                                                  image: CachedNetworkImageProvider(
-                                                    model.items[index].user.avatar,
-                                                  )),
-                                            ),
+                                          child: CircleAvatar(
+                                            backgroundImage: userAvatarProvider(model.items[index].user.avatar),
+                                            radius: 22,
                                           ),
                                           onTap: () {
                                             NavigatorUtil.goPage(

+ 0 - 14
lib/pages/sport/calorie_page.dart

@@ -1,14 +0,0 @@
-import 'package:flutter/material.dart';
-
-class CaloriePage extends StatefulWidget {
-  @override
-  State<StatefulWidget> createState() => _PageState();
-}
-
-class _PageState extends State<CaloriePage> {
-  @override
-  Widget build(BuildContext context) {
-// TODO: implement build
-    throw UnimplementedError();
-  }
-}

+ 0 - 14
lib/pages/sport/intensity_page.dart

@@ -1,14 +0,0 @@
-import 'package:flutter/material.dart';
-
-class IntensityPage extends StatefulWidget {
-  @override
-  State<StatefulWidget> createState() => _PageState();
-}
-
-class _PageState extends State<IntensityPage> {
-  @override
-  Widget build(BuildContext context) {
-// TODO: implement build
-    throw UnimplementedError();
-  }
-}

+ 0 - 89
lib/pages/sport/target_page.dart

@@ -1,89 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:percent_indicator/circular_percent_indicator.dart';
-import 'package:sport/widgets/decoration.dart';
-import 'package:sport/widgets/progress_bar.dart';
-
-class TargetPage extends StatefulWidget {
-  @override
-  State<StatefulWidget> createState() => _PageState();
-}
-
-class _PageState extends State<TargetPage>  with TickerProviderStateMixin{
-
-  AnimationController animationController;
-  Animation animation;
-  @override
-  void initState() {
-    super.initState();
-    animationController =
-    AnimationController(duration: Duration(seconds: 1), vsync: this)
-      ..addStatusListener((status) {
-
-      });
-    animation = Tween(begin: 0.0, end: 1.0).animate(animationController);
-    //开始动画
-    animationController.forward();
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    return Scaffold(
-      body: SingleChildScrollView(
-        child: Column(
-          children: <Widget>[
-            Container(
-                margin: EdgeInsets.fromLTRB(12.0, 6.0, 12.0, 18.0),
-                padding: EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 12.0),
-                decoration: circular(),
-                child: Column(
-                  children: <Widget>[
-                    Row(
-                      children: <Widget>[
-                        Text("目标状态:"),
-                        Text("目标状态:"),
-                      ],
-                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
-                    )
-                  ],
-                )),
-            CalendarDatePicker(
-              initialDate: DateTime.now(),
-              firstDate: DateTime(2020, 1, 1),
-              lastDate: DateTime.now(),
-              onDateChanged: (t) {},
-              initialCalendarMode: DatePickerMode.day,
-            ),
-            CircularPercentIndicator(
-              radius: 100.0,
-              lineWidth: 10.0,
-              percent: 0.7,
-              center: new Text("100%"),
-              animation: true,
-              animationDuration: 1000,
-              animateFromLastPercent: true,
-              circularStrokeCap: CircularStrokeCap.round,
-              rotateLinearGradient: true,
-              linearGradient: LinearGradient(
-                colors: <Color>[Color(0xff8DF7FF), Color(0xff16A2FF)],
-              ),
-            ),
-
-            CustomPaint(
-              painter: ProgressBarThree(.7),
-              child: SizedBox(width: 100, height: 100,),
-            ),
-            AnimatedBuilder(
-              animation: animation,
-              builder: (BuildContext context, Widget child) {
-                return CustomPaint(
-                  painter: ProgressBarThree(animation.value),
-                  child: SizedBox(width: 100, height: 100,),
-                );
-              },
-            )
-          ],
-        ),
-      ),
-    );
-  }
-}

+ 1 - 1
lib/provider/bluetooth.dart

@@ -336,7 +336,7 @@ class Bluetooth with ChangeNotifier, InjectApi {
         dataNotifier.value = info;
         electricityNotifier.value = min(info['0_electricity'] ?? 0, info['1_electricity'] ?? 0);
 
-        _testElectricity(info);
+        // _testElectricity(info);
 
         break;
       case 0x02: // 查询步数

+ 6 - 1
lib/provider/message_model.dart

@@ -11,6 +11,7 @@ class MessageModel extends ChangeNotifier with InjectApi {
 //  Message _message;
   int _curId;
   List<MessageInstance> _messages;
+  AppLifecycleState state;
 
   final StreamController<List<MessageInstance>> _queryController =
       StreamController.broadcast();
@@ -24,11 +25,15 @@ class MessageModel extends ChangeNotifier with InjectApi {
   init() {
     periodicSubscription =
         Stream.periodic(Duration(seconds: 10)).listen((event) async {
+
+          if(state == AppLifecycleState.paused)
+            return;
+
       Message data;
 
       SharedPreferences prefs = await SharedPreferences.getInstance();
       String token = prefs.getString("token");
-      if(token == null)
+      if(token == null || token.isEmpty == true)
         return;
 
       if (_curId != null) {

+ 27 - 28
lib/widgets/appbar.dart

@@ -2,10 +2,7 @@ import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:sport/widgets/image.dart';
 
-import 'misc.dart';
-
-const titleStyle = TextStyle(
-    fontWeight: FontWeight.w600, fontSize: 18.0, color: Color(0xff333333));
+const titleStyle = TextStyle(fontWeight: FontWeight.w600, fontSize: 18.0, color: Color(0xff333333));
 
 Widget buildSliverAppBar(BuildContext context, String title,
     {List<Widget> actions,
@@ -18,12 +15,11 @@ Widget buildSliverAppBar(BuildContext context, String title,
     brightness = -1,
     textStyle = titleStyle,
     whiteBackButton = false,
-    pinned = true}) {
+    pinned = true,
+    EdgeInsets padding = const EdgeInsets.fromLTRB(12.0, 0, 0, 15.0)}) {
   return SliverAppBar(
       pinned: pinned,
-      brightness: brightness == -1
-          ? null
-          : brightness == 1 ? Brightness.dark : Brightness.light,
+      brightness: brightness == -1 ? null : brightness == 1 ? Brightness.dark : Brightness.light,
       expandedHeight: height,
       backgroundColor: backgroundColor,
       forceElevated: innerBoxIsScrolled,
@@ -45,37 +41,28 @@ Widget buildSliverAppBar(BuildContext context, String title,
                 )
               : whiteBackButton
                   ? IconButton(
-                      icon:
-                          Image.asset("lib/assets/img/topbar_return_white.png"),
+                      icon: Image.asset("lib/assets/img/topbar_return_white.png"),
                       onPressed: () {
                         Navigator.of(context).pop();
                       },
                     )
                   : Container(),
       actions: actions,
-      flexibleSpace: buildFlexibleSpace(title,
-          paddingLeading: paddingLeading, textStyle: textStyle));
+      flexibleSpace: _buildFlexibleSpace(title, paddingLeading: paddingLeading, textStyle: textStyle, padding: padding));
 }
 
-Widget buildFlexibleSpace(
-  String title, {
-  paddingLeading = true,
-  textStyle = titleStyle,
-}) {
+Widget _buildFlexibleSpace(String title, {paddingLeading = true, textStyle = titleStyle, EdgeInsets padding}) {
   return LayoutBuilder(builder: (context, box) {
-    final FlexibleSpaceBarSettings settings =
-        context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
+    final FlexibleSpaceBarSettings settings = context.dependOnInheritedWidgetOfExactType<FlexibleSpaceBarSettings>();
     return FlexibleSpaceBar(
       centerTitle: false,
-      titlePadding: EdgeInsets.fromLTRB(12.0, 0, 0, 15.0),
+      titlePadding: padding,
       title: paddingLeading
           ? Padding(
-              padding: EdgeInsets.only(
-                  left: (settings.maxExtent - box.biggest.height) / 3 * 1.6,bottom: 1.0),
+              padding: EdgeInsets.only(left: (settings.maxExtent - box.biggest.height) / 3 * 1.6, bottom: .5),
               child: Text(
                 title,
                 style: textStyle,
-//                strutStyle:fixedLine,
               ),
             )
           : Text(
@@ -86,14 +73,11 @@ Widget buildFlexibleSpace(
   });
 }
 
-Widget buildActionButton(String title, VoidCallback onPressed,
-    {Color textColor}) {
+Widget buildActionButton(String title, VoidCallback onPressed, {Color textColor}) {
   return IconButton(
     icon: Text(
       title,
-      style: textColor != null
-          ? TextStyle(fontSize: 16.0, color: textColor)
-          : TextStyle(fontSize: 16),
+      style: textColor != null ? TextStyle(fontSize: 16.0, color: textColor) : TextStyle(fontSize: 16),
     ),
     onPressed: onPressed,
   );
@@ -107,3 +91,18 @@ Widget buildBackButton(BuildContext context) {
     },
   );
 }
+
+Widget buildAppBar(BuildContext context, {String title, List<Widget> actions}) {
+  return AppBar(
+    titleSpacing: -16.0,
+    centerTitle: false,
+    title: title == null
+        ? null
+        : Text(
+            "$title",
+            style: titleStyle,
+          ),
+    leading: buildBackButton(context),
+    actions: actions,
+  );
+}

+ 4 - 4
lib/widgets/chart.dart

@@ -43,7 +43,7 @@ class Chart extends CustomPainter {
   );
 
   double _zero = 0;
-  double _paddingLeft = 30;
+  double _paddingLeft = 35;
   double _paddingRight = 10;
   double _labelHeight = 30;
 
@@ -103,7 +103,7 @@ class Chart extends CustomPainter {
     });
     double diff = _max % _valueSplit;
     _max = max(maxValue, _max + diff);
-    // print("$values --- $_max $maxValue $diff");
+    print("$values --- $_max $maxValue $diff");
   }
 
   @override
@@ -248,7 +248,7 @@ class Chart extends CustomPainter {
             paragraph = unit.build()..layout(pc);
             paragraph.computeLineMetrics().forEach((element) {
               var x = l + valueStroke / 2 - element.width / 2;
-              canvas.drawParagraph(paragraph, Offset(x, offsetY + (offsetY < 1 ? element.baseline : -element.baseline) - 10));
+              canvas.drawParagraph(paragraph, Offset(x, offsetY -element.baseline - 10));
               x = l + valueStroke / 2 ;
               canvas.drawPath(Path()..moveTo(x, offsetY - 4)..lineTo(x - 3, offsetY - 7)..lineTo(x + 3, offsetY - 7)..close(), _maxPaint);
             });
@@ -287,7 +287,7 @@ class Chart extends CustomPainter {
 
   double calValue(num value) {
     if (value == null) return _zero;
-    return min(_zero, _zero - _zero * value / _max);
+    return max(0, _zero - _zero * value / _max);
   }
 
   @override

+ 493 - 0
lib/widgets/circular_percent_indicator.dart

@@ -0,0 +1,493 @@
+//import 'dart:math';
+
+import 'package:flutter/material.dart';
+import 'dart:math' as math;
+
+enum CircularStrokeCap { butt, round, square }
+
+enum ArcType { HALF, FULL, CUSTOM }
+
+// ignore: must_be_immutable
+class CircularPercentIndicator extends StatefulWidget {
+  ///Percent value between 0.0 and 1.0
+  final double percent;
+  final double radius;
+
+  ///Width of the progress bar of the circle
+  final double lineWidth;
+
+  ///Width of the unfilled background of the progress bar
+  final double backgroundWidth;
+
+  ///Color of the background of the circle , default = transparent
+  final Color fillColor;
+
+  ///First color applied to the complete circle
+  final Color backgroundColor;
+
+  Color get progressColor => _progressColor;
+
+  Color _progressColor;
+
+  ///true if you want the circle to have animation
+  final bool animation;
+
+  ///duration of the animation in milliseconds, It only applies if animation attribute is true
+  final int animationDuration;
+
+  ///widget at the top of the circle
+  final Widget header;
+
+  ///widget at the bottom of the circle
+  final Widget footer;
+
+  ///widget inside the circle
+  final Widget center;
+
+  final LinearGradient linearGradient;
+
+  ///The kind of finish to place on the end of lines drawn, values supported: butt, round, square
+  final CircularStrokeCap circularStrokeCap;
+
+  ///the angle which the circle will start the progress (in degrees, eg: 0.0, 45.0, 90.0)
+  final double startAngle;
+
+  /// set true if you want to animate the linear from the last percent value you set
+  final bool animateFromLastPercent;
+
+  /// set false if you don't want to preserve the state of the widget
+  final bool addAutomaticKeepAlive;
+
+  /// set the arc type
+  final ArcType arcType;
+
+  /// set a circular background color when use the arcType property
+  final Color arcBackgroundColor;
+
+  /// set true when you want to display the progress in reverse mode
+  final bool reverse;
+
+  /// Creates a mask filter that takes the progress shape being drawn and blurs it.
+  final MaskFilter maskFilter;
+
+  /// set a circular curve animation type
+  final Curve curve;
+
+  /// set true when you want to restart the animation, it restarts only when reaches 1.0 as a value
+  /// defaults to false
+  final bool restartAnimation;
+
+  /// Callback called when the animation ends (only if `animation` is true)
+  final VoidCallback onAnimationEnd;
+
+  /// Display a widget indicator at the end of the progress. It only works when `animation` is true
+  final Widget widgetIndicator;
+
+  /// Set to true if you want to rotate linear gradient in accordance to the [startAngle].
+  final bool rotateLinearGradient;
+
+  CircularPercentIndicator(
+      {Key key,
+      this.percent = 0.0,
+      this.lineWidth = 5.0,
+      this.startAngle = 0.0,
+      @required this.radius,
+      this.fillColor = Colors.transparent,
+      this.backgroundColor = const Color(0xFFB8C7CB),
+      Color progressColor,
+      this.backgroundWidth = -1, //negative values ignored, replaced with lineWidth
+      this.linearGradient,
+      this.animation = false,
+      this.animationDuration = 500,
+      this.header,
+      this.footer,
+      this.center,
+      this.addAutomaticKeepAlive = true,
+      this.circularStrokeCap,
+      this.arcBackgroundColor,
+      this.arcType,
+      this.animateFromLastPercent = false,
+      this.reverse = false,
+      this.curve = Curves.linear,
+      this.maskFilter,
+      this.restartAnimation = false,
+      this.onAnimationEnd,
+      this.widgetIndicator,
+      this.rotateLinearGradient = false})
+      : super(key: key) {
+    if (linearGradient != null && progressColor != null) {
+      throw ArgumentError('Cannot provide both linearGradient and progressColor');
+    }
+    _progressColor = progressColor ?? Colors.red;
+
+    assert(startAngle >= 0.0);
+    assert(curve != null);
+    if (percent < 0.0) {
+      throw Exception("Percent value must be a double between 0.0 and 1.0");
+    }
+
+    if (arcType == null && arcBackgroundColor != null) {
+      throw ArgumentError('arcType is required when you arcBackgroundColor');
+    }
+  }
+
+  @override
+  _CircularPercentIndicatorState createState() => _CircularPercentIndicatorState();
+}
+
+class _CircularPercentIndicatorState extends State<CircularPercentIndicator> with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {
+  AnimationController _animationController;
+  Animation _animation;
+  double _percent = 0.0;
+
+  @override
+  void dispose() {
+    if (_animationController != null) {
+      _animationController.dispose();
+    }
+    super.dispose();
+  }
+
+  @override
+  void initState() {
+    if (widget.animation) {
+      _animationController = AnimationController(vsync: this, duration: Duration(milliseconds: widget.animationDuration));
+      _animation = Tween(begin: 0.0, end: widget.percent).animate(
+        CurvedAnimation(parent: _animationController, curve: widget.curve),
+      )..addListener(() {
+          setState(() {
+            _percent = _animation.value;
+          });
+          if (widget.restartAnimation && _percent == 1.0) {
+            _animationController.repeat(min: 0, max: 1.0);
+          }
+        });
+      _animationController.addStatusListener((status) {
+        if (widget.onAnimationEnd != null && status == AnimationStatus.completed) {
+          widget.onAnimationEnd();
+        }
+      });
+      _animationController.forward();
+    } else {
+      _updateProgress();
+    }
+    super.initState();
+  }
+
+  void _checkIfNeedCancelAnimation(CircularPercentIndicator oldWidget) {
+    if (oldWidget.animation && !widget.animation && _animationController != null) {
+      _animationController.stop();
+    }
+  }
+
+  @override
+  void didUpdateWidget(CircularPercentIndicator oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (oldWidget.percent != widget.percent || oldWidget.startAngle != widget.startAngle) {
+      if (_animationController != null) {
+        _animationController.duration = Duration(milliseconds: widget.animationDuration);
+        _animation = Tween(begin: widget.animateFromLastPercent ? oldWidget.percent : 0.0, end: widget.percent).animate(
+          CurvedAnimation(parent: _animationController, curve: widget.curve),
+        );
+        _animationController.forward(from: 0.0);
+      } else {
+        _updateProgress();
+      }
+    }
+    _checkIfNeedCancelAnimation(oldWidget);
+  }
+
+  _updateProgress() {
+    setState(() {
+      _percent = widget.percent;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    super.build(context);
+    var items = List<Widget>();
+    if (widget.header != null) {
+      items.add(widget.header);
+    }
+    items.add(
+      Container(
+        height: widget.radius,
+        width: widget.radius,
+        child: Stack(
+          children: [
+            CustomPaint(
+              painter: CirclePainter(
+                  progress: math.min(360, _percent * 360),
+                  progressColor: widget.progressColor,
+                  progressOver: math.max(0, _percent -1),
+                  backgroundColor: widget.backgroundColor,
+                  startAngle: widget.startAngle,
+                  circularStrokeCap: widget.circularStrokeCap,
+                  radius: (widget.radius / 2) - widget.lineWidth / 2,
+                  lineWidth: widget.lineWidth,
+                  backgroundWidth: //negative values ignored, replaced with lineWidth
+                      widget.backgroundWidth >= 0.0 ? (widget.backgroundWidth) : widget.lineWidth,
+                  arcBackgroundColor: widget.arcBackgroundColor,
+                  arcType: widget.arcType,
+                  reverse: widget.reverse,
+                  linearGradient: widget.linearGradient,
+                  maskFilter: widget.maskFilter,
+                  rotateLinearGradient: widget.rotateLinearGradient),
+              child: (widget.center != null) ? Center(child: widget.center) : Container(),
+            ),
+            if (widget.widgetIndicator != null && widget.animation)
+              Positioned.fill(
+                child: Transform.rotate(
+                  angle: radians((widget.circularStrokeCap != CircularStrokeCap.butt && widget.reverse) ? -15 : 0),
+                  child: Transform.rotate(
+                    angle: radians((widget.reverse ? -360 : 360) * _percent),
+                    child: Transform.translate(
+                      offset: Offset(
+                        (widget.circularStrokeCap != CircularStrokeCap.butt) ? widget.lineWidth / 2 : 0,
+                        (-widget.radius / 2 + widget.lineWidth / 2),
+                      ),
+                      child: widget.widgetIndicator,
+                    ),
+                  ),
+                ),
+              ),
+          ],
+        ),
+      ),
+    );
+
+    if (widget.footer != null) {
+      items.add(widget.footer);
+    }
+
+    return Material(
+      color: widget.fillColor,
+      child: Container(
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          mainAxisSize: MainAxisSize.min,
+          children: items,
+        ),
+      ),
+    );
+  }
+
+  @override
+  bool get wantKeepAlive => widget.addAutomaticKeepAlive;
+}
+
+class CirclePainter extends CustomPainter {
+  final Paint _paintBackground = Paint();
+  final Paint _paintLine = Paint();
+  final Paint _paintBackgroundStartAngle = Paint();
+  final double lineWidth;
+  final double backgroundWidth;
+  final double progress;
+  final double progressOver;
+  final double radius;
+  final Color progressColor;
+  final Color backgroundColor;
+  final CircularStrokeCap circularStrokeCap;
+  final double startAngle;
+  final LinearGradient linearGradient;
+  final Color arcBackgroundColor;
+  final ArcType arcType;
+  final bool reverse;
+  final MaskFilter maskFilter;
+  final bool rotateLinearGradient;
+
+  final Paint _paintScale = Paint()..strokeWidth = 1;
+
+  CirclePainter(
+      {this.lineWidth,
+      this.backgroundWidth,
+      this.progress,
+        this.progressOver,
+      @required this.radius,
+      this.progressColor,
+      this.backgroundColor,
+      this.startAngle = 0.0,
+      this.circularStrokeCap = CircularStrokeCap.round,
+      this.linearGradient,
+      this.reverse,
+      this.arcBackgroundColor,
+      this.arcType,
+      this.maskFilter,
+      this.rotateLinearGradient}) {
+    _paintBackground.color = backgroundColor;
+    _paintBackground.style = PaintingStyle.stroke;
+    _paintBackground.strokeWidth = backgroundWidth;
+    if (circularStrokeCap == CircularStrokeCap.round) {
+      _paintBackground.strokeCap = StrokeCap.round;
+    } else if (circularStrokeCap == CircularStrokeCap.butt) {
+      _paintBackground.strokeCap = StrokeCap.butt;
+    } else {
+      _paintBackground.strokeCap = StrokeCap.square;
+    }
+    if (arcBackgroundColor != null) {
+      _paintBackgroundStartAngle.color = arcBackgroundColor;
+      _paintBackgroundStartAngle.style = PaintingStyle.stroke;
+      _paintBackgroundStartAngle.strokeWidth = lineWidth;
+      if (circularStrokeCap == CircularStrokeCap.round) {
+        _paintBackgroundStartAngle.strokeCap = StrokeCap.round;
+      } else if (circularStrokeCap == CircularStrokeCap.butt) {
+        _paintBackgroundStartAngle.strokeCap = StrokeCap.butt;
+      } else {
+        _paintBackgroundStartAngle.strokeCap = StrokeCap.square;
+      }
+    }
+
+    _paintLine.color = progressColor;
+    _paintLine.style = PaintingStyle.stroke;
+    _paintLine.strokeWidth = lineWidth;
+    _paintLine.strokeJoin = StrokeJoin.round;
+    if (circularStrokeCap == CircularStrokeCap.round) {
+      _paintLine.strokeCap = StrokeCap.round;
+    } else if (circularStrokeCap == CircularStrokeCap.butt) {
+      _paintLine.strokeCap = StrokeCap.butt;
+    } else {
+      _paintLine.strokeCap = StrokeCap.square;
+    }
+  }
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final center = Offset(size.width / 2, size.height / 2);
+    double fixedStartAngle = startAngle;
+    final rectForArc = Rect.fromCircle(center: center, radius: radius);
+    double startAngleFixedMargin = 1.0;
+    if (arcType != null) {
+      if (arcType == ArcType.CUSTOM) {
+        startAngleFixedMargin = 1 - (fixedStartAngle - 180).abs() * 2 / 360;
+      } else if (arcType == ArcType.FULL) {
+        fixedStartAngle = 220;
+        startAngleFixedMargin = 172 / fixedStartAngle;
+      } else {
+        fixedStartAngle = 270;
+        startAngleFixedMargin = 135 / fixedStartAngle;
+      }
+    }
+    if (arcType == ArcType.HALF) {
+      canvas.drawArc(rectForArc, radians(-90.0 + fixedStartAngle), radians(360 * startAngleFixedMargin), false, _paintBackground);
+    } else if (arcType == ArcType.CUSTOM) {
+      canvas.drawArc(rectForArc, radians(-90.0 + fixedStartAngle), radians(360 * startAngleFixedMargin), false, _paintBackground);
+    } else {
+      canvas.drawCircle(center, radius, _paintBackground);
+    }
+
+    if (maskFilter != null) {
+      _paintLine.maskFilter = maskFilter;
+    }
+    if (linearGradient != null) {
+      if (rotateLinearGradient && progress > 0) {
+        double correction = 0;
+        if (_paintLine.strokeCap == StrokeCap.round || _paintLine.strokeCap == StrokeCap.square) {
+          if (reverse) {
+            correction = math.atan(_paintLine.strokeWidth / 2 / radius);
+          } else {
+            correction = math.atan(_paintLine.strokeWidth / 2 / radius);
+          }
+        }
+        _paintLine.shader = SweepGradient(
+                transform:
+                    reverse ? GradientRotation(radians(-90 - progress + startAngle) - correction) : GradientRotation(radians(-90.0 + startAngle) - correction),
+                startAngle: radians(0),
+                endAngle: radians(360),
+                tileMode: TileMode.clamp,
+                colors: reverse ? linearGradient.colors.reversed.toList() : linearGradient.colors)
+            .createShader(
+          Rect.fromCircle(
+            center: center,
+            radius: radius,
+          ),
+        );
+      } else if (!rotateLinearGradient) {
+        _paintLine.shader = linearGradient.createShader(
+          Rect.fromCircle(
+            center: center,
+            radius: radius,
+          ),
+        );
+      }
+    }
+
+    if (arcBackgroundColor != null) {
+      canvas.drawArc(
+        Rect.fromCircle(center: center, radius: radius),
+        radians(-90.0 + fixedStartAngle),
+        radians(360 * startAngleFixedMargin),
+        false,
+        _paintBackgroundStartAngle,
+      );
+    }
+
+    if (arcType == ArcType.CUSTOM) {
+      if(progressOver > 0) {
+        canvas.save();
+        var rect = RRect.fromRectAndRadius(Rect.fromCenter(center: Offset(center.dy, lineWidth + 4), width: 2, height: 3), Radius.circular(5.0));
+        canvas.translate(center.dx, center.dy);
+        canvas.rotate(radians(fixedStartAngle));
+        canvas.translate(-center.dx, -center.dy);
+        var count = 60;
+        var r = radians(360 * startAngleFixedMargin) / count;
+        for (var i = 0; i <= count; i++) {
+          if (progressOver >= i / count) {
+            _paintScale.color = _setColor(i, count);
+          } else {
+            _paintScale.color = backgroundColor;
+          }
+          canvas.drawRRect(rect, _paintScale);
+          canvas.translate(center.dx, center.dy);
+          canvas.rotate(r);
+          canvas.translate(-center.dx, -center.dy);
+        }
+        canvas.restore();
+      }
+    }
+
+    if (reverse) {
+      final start = radians(360 * startAngleFixedMargin - 90.0 + fixedStartAngle);
+      final end = radians(-progress * startAngleFixedMargin);
+      canvas.drawArc(
+        Rect.fromCircle(
+          center: center,
+          radius: radius,
+        ),
+        start,
+        end,
+        false,
+        _paintLine,
+      );
+    } else {
+      final start = radians(-90.0 + fixedStartAngle);
+      final end = radians(progress * startAngleFixedMargin);
+      canvas.drawArc(
+        Rect.fromCircle(
+          center: center,
+          radius: radius,
+        ),
+        start,
+        end,
+        false,
+        _paintLine,
+      );
+    }
+  }
+
+  final Color _o = const Color(0xff8DF7FF);
+  final Color _o1 = const Color(0xff16A2FF);
+  _setColor(int val,int mCount) {
+    int r = 0, g = 0, b = 0;
+    r = (_o.red + (_o1.red - _o.red) * val / mCount).toInt();
+    g = (_o.green + (_o1.green - _o.green) * val / mCount).toInt();
+    b = (_o.blue + (_o1.blue - _o.blue) * val / mCount).toInt();
+    return Color.fromRGBO(r,g,b, 1.0);
+  }
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) {
+    return true;
+  }
+}
+
+num radians(num deg) => deg * (math.pi / 180.0);

+ 25 - 14
lib/widgets/dialog/search_device.dart

@@ -643,15 +643,25 @@ class ScanResultTile extends StatelessWidget {
         initialData: BluetoothDeviceState.connecting,
         builder: (c, snapshot) {
           if (snapshot.data == BluetoothDeviceState.connected) {
-            return Container(
-              width: 64,
-              height: 30,
-              alignment: Alignment.center,
-              decoration: BoxDecoration(shape: BoxShape.rectangle, borderRadius: BorderRadius.all(Radius.circular(100)), color: Theme.of(context).accentColor),
-              child: Text(
-                snapshot.data == BluetoothDeviceState.connected ? "已连接" : "未连接",
-                style: TextStyle(color: Colors.white, fontSize: 12),
-              ),
+            return Row(
+              mainAxisSize: MainAxisSize.min,
+              children: <Widget>[
+                Image.asset(
+                  "lib/assets/img/pop_icon_conneted.png",
+                  width: 14.0,
+                ),
+                SizedBox(
+                  width: 4,
+                ),
+                Text(
+                  "已连接",
+                  style: TextStyle(
+                    color: Theme.of(context).accentColor,
+                    fontSize: 12,
+                  ),
+                  strutStyle: fixedLine,
+                )
+              ],
             );
           }
           int status = Provider.of<Bluetooth>(context, listen: false).device == result.device
@@ -662,12 +672,13 @@ class ScanResultTile extends StatelessWidget {
           return status == 1
               ? Padding(
                   padding: const EdgeInsets.symmetric(horizontal: 4.0),
-                  child: SizedBox(
-                    width: 12,
-                    height: 12,
-                    child: CircularProgressIndicator(
-                      strokeWidth: 2,
+                  child: Text(
+                    "正在连接...",
+                    style: TextStyle(
+                      color: Theme.of(context).accentColor,
+                      fontSize: 12,
                     ),
+                    strutStyle: fixedLine,
                   ),
                 )
               : GestureDetector(

+ 30 - 21
lib/widgets/dialog/share_popup.dart

@@ -1,4 +1,5 @@
 import 'dart:math';
+import 'dart:ui';
 
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:flutter/cupertino.dart';
@@ -268,28 +269,36 @@ class _sharePopupState extends State<sharePopup> {
   Widget build(BuildContext context) {
     print(widget.list);
     // TODO: implement build
-    return Material(
-      color: Colors.black.withOpacity(0.5),
-      child: Column(
-        mainAxisAlignment: MainAxisAlignment.start,
-        children: <Widget>[
-          Expanded(
-            child: Center(
-              // stack 需要一个高度进行搞一搞
-              child: Stack(
-                children: widget.list.reversed.map((e) {
-                  return Transform.translate(
-                      offset: Offset((widget.list.indexOf(e) + 1) * 5.0,
-                          -(widget.list.indexOf(e)) * 5.0),
+    return BackdropFilter(
+      //背景滤镜
+      filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
+      child: Scaffold(
+          backgroundColor: Colors.black.withOpacity(0.8),
+          body: GestureDetector(
+              onTap: () {
+                Navigator.maybePop(context);
+              },
+              behavior: HitTestBehavior.opaque,
+              child: Column(
+                mainAxisAlignment: MainAxisAlignment.start,
+                children: <Widget>[
+                  Expanded(
+                    child: Center(
+                      // stack 需要一个高度进行搞一搞
+                      child: Stack(
+                        children: widget.list.reversed.map((e) {
+                          return Transform.translate(
+                              offset: Offset((widget.list.indexOf(e) + 1) * 5.0,
+                                  -(widget.list.indexOf(e)) * 5.0),
 //                      padding: EdgeInsets.only(
 //                          right: (data.indexOf(e)) * 10.0,
 //                          top: (data.indexOf(e)) * 10.0),
-                      child: content(
-                          e, widget.list.indexOf(e), widget.list.length));
-                }).toList(),
-              ),
-            ),
-          ),
+                              child: content(e, widget.list.indexOf(e),
+                                  widget.list.length));
+                        }).toList(),
+                      ),
+                    ),
+                  ),
 //          if (data.length > 0)
 //            InkWell(
 //              child: Image.asset("lib/assets/img/pop_share_chose.png"),
@@ -303,8 +312,8 @@ class _sharePopupState extends State<sharePopup> {
 //                }
 //              },
 //            )
-        ],
-      ),
+                ],
+              ))),
     );
   }
 }

+ 8 - 6
lib/widgets/loading.dart

@@ -1,15 +1,17 @@
-import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_easyrefresh/easy_refresh.dart';
 import 'package:flutter_spinkit/flutter_spinkit.dart';
+import 'package:sport/widgets/misc.dart';
+import 'package:sport/widgets/refresh_header.dart' as loading;
 
 class RequestLoadingWidget extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
-    return Padding(
-      padding: const EdgeInsets.symmetric(vertical: 50.0),
-      child: SpinKitWave(
-        color: Theme.of(context).accentColor,
-      ),
+    return Container(
+      height: 100.0,
+      // padding: const EdgeInsets.symmetric(vertical: 50.0),
+      child: loading.ClassicalHeaderWidget(refreshState: RefreshMode.refresh, pulledExtent: 120.0, refreshTriggerPullDistance: 100.0, refreshIndicatorExtent: 80.0, classicalHeader: buildClassicalHeader(),
+      axisDirection: AxisDirection.down,noMore: false, enableInfiniteRefresh: true,),
     );
   }
 }

+ 60 - 107
lib/widgets/menu_bar.dart

@@ -48,8 +48,7 @@ class MenuBar extends StatefulWidget {
   }
 }
 
-class _MenuBarState extends State<MenuBar>
-    with WidgetsBindingObserver, InjectApi {
+class _MenuBarState extends State<MenuBar> with WidgetsBindingObserver, InjectApi {
   GlobalKey _myKey = new GlobalKey(); // 用来定位Message位置
 
   List<Asset> imageList = []; // 选图片的列表
@@ -62,6 +61,8 @@ class _MenuBarState extends State<MenuBar>
 
   FocusNode _focusNode = new FocusNode(); // TextField 的 focus
 
+  ValueNotifier<bool> _postable = ValueNotifier(false);
+
   double keyBoardHeight = 270.0; // 初始化下面menu的高度 后续会动态调整后 优化
 
   bool isFirst = true; // flag 优化menu高度的操作
@@ -87,7 +88,10 @@ class _MenuBarState extends State<MenuBar>
 
     initEmoji();
 
-    _controller = TextEditingController();
+    _controller = TextEditingController()
+      ..addListener(() {
+        _postable.value = _controller.value.text.isNotEmpty;
+      });
 //    _focusNode.addListener(() {
 //      if (_focusNode.hasFocus) {
 ////        setState(() {
@@ -99,8 +103,7 @@ class _MenuBarState extends State<MenuBar>
 
   // 直接进来就请求了 不要 搞这些骚的....
   void initEmoji() async {
-    String json = await DefaultAssetBundle.of(context)
-        .loadString("lib/assets/json/emoji_list.json");
+    String json = await DefaultAssetBundle.of(context).loadString("lib/assets/json/emoji_list.json");
     setState(() {
       emojiJson = json;
     });
@@ -111,21 +114,21 @@ class _MenuBarState extends State<MenuBar>
   void dispose() {
     super.dispose();
     //释放
-    _focusNode.dispose();
+    _focusNode?.dispose();
     _timer?.cancel();
+    _postable?.dispose();
+    _controller?.dispose();
   }
 
   // 这里 插库 + 渲染更新...
   Future add(MessageInstance message) async {
     // 聊天的时候 是没有返回 curId的 本地存储的时候 就得自己构造一个? 或者是不存?
-    await MessageDB().insert(new MessageItem(
-        message: message, status: 0, userId: message.toUser.id, curId: 0));
+    await MessageDB().insert(new MessageItem(message: message, status: 0, userId: message.toUser.id, curId: 0));
 
     var list = await MessageDB().findHasUserId(message.toUser.id);
 
     if (list.length == 0) {
-      await MessageDB()
-          .insertUser(new UserTableInfo(userId: message.toUser.id, isTop: 0));
+      await MessageDB().insertUser(new UserTableInfo(userId: message.toUser.id, isTop: 0));
     }
 
     // view 上setstate 渲染的...
@@ -134,8 +137,7 @@ class _MenuBarState extends State<MenuBar>
 
   _postFeedBackpostFeedBack(String content, {int typeId = 0}) async {
     typeId == null ? typeId = 0 : typeId = typeId;
-    await api.postFeedback(typeId, content,
-        extra: await Application.getDeviceInfo());
+    await api.postFeedback(typeId, content, extra: await Application.getDeviceInfo());
   }
 
   Widget extMenuItem(
@@ -174,8 +176,7 @@ class _MenuBarState extends State<MenuBar>
           for (var i = 0; i < resultList.length; i++) {
             Asset asset = resultList[i];
             ByteData byteData = await asset.getByteData(quality: 85);
-            File file = File(
-                '${directory.path}/${DateTime.now().millisecondsSinceEpoch}_$i.jpg');
+            File file = File('${directory.path}/${DateTime.now().millisecondsSinceEpoch}_$i.jpg');
             List<int> bytes = byteData.buffer.asUint8List().toList();
             print('临时文件 ${file.path} ${bytes.length}');
             file.writeAsBytesSync(bytes);
@@ -188,11 +189,7 @@ class _MenuBarState extends State<MenuBar>
         if (widget.menuIdentity.menuScene == "chat") {
           if (urls.length > 0) {
             for (int i = 0; i < urls.length; i++) {
-              MessageInstance message = (await api.postChatSend(
-                      widget.menuIdentity.userId,
-                      "image",
-                      '{"url":"${urls[i]}"}'))
-                  .data;
+              MessageInstance message = (await api.postChatSend(widget.menuIdentity.userId, "image", '{"url":"${urls[i]}"}')).data;
               add(message);
             }
           }
@@ -205,8 +202,7 @@ class _MenuBarState extends State<MenuBar>
     void getPhoto() async {
       try {
         // 拍完直接发...
-        final pickedFile = await new ImagePicker()
-            .getImage(source: ImageSource.camera, imageQuality: 70);
+        final pickedFile = await new ImagePicker().getImage(source: ImageSource.camera, imageQuality: 70);
 
         // 如果是聊天 直接发 ...
         if (widget.menuIdentity.menuScene == "chat") {
@@ -215,11 +211,7 @@ class _MenuBarState extends State<MenuBar>
             var data = (await api.postChatUpload(File(pickedFile.path))).data;
             print(data['url']);
 //          print("${data}-----------------------------");
-            MessageInstance message = (await api.postChatSend(
-                    widget.menuIdentity.userId,
-                    "image",
-                    '{ "url":"${data['url']}" }'))
-                .data;
+            MessageInstance message = (await api.postChatSend(widget.menuIdentity.userId, "image", '{ "url":"${data['url']}" }')).data;
             add(message);
           });
         }
@@ -260,10 +252,7 @@ class _MenuBarState extends State<MenuBar>
       return Container(
         height: keyBoardHeight,
         padding: EdgeInsets.only(left: 5, top: 5, right: 5, bottom: 5),
-        decoration: BoxDecoration(
-            color: Colors.white,
-            border:
-                Border(top: BorderSide(width: 1.0, color: Color(0xFFDCDCDC)))),
+        decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(width: 1.0, color: Color(0xFFDCDCDC)))),
         child: GridView.custom(
           padding: EdgeInsets.all(3),
           shrinkWrap: true,
@@ -276,17 +265,14 @@ class _MenuBarState extends State<MenuBar>
             (context, index) {
               return GestureDetector(
                 onTap: () {
-                  String intPutString = _controller.text +
-                      String.fromCharCode(data[index]["unicode"]);
+                  String intPutString = _controller.text + String.fromCharCode(data[index]["unicode"]);
 
                   var content = intPutString;
                   _controller.value = TextEditingValue(
                       // 设置内容
                       text: content,
                       // 保持光标在最后
-                      selection: TextSelection.fromPosition(TextPosition(
-                          affinity: TextAffinity.downstream,
-                          offset: content.length)));
+                      selection: TextSelection.fromPosition(TextPosition(affinity: TextAffinity.downstream, offset: content.length)));
                   // 主要是 onchange 没有办法 加上 表情 ...
                   setState(() {});
                 },
@@ -316,10 +302,7 @@ class _MenuBarState extends State<MenuBar>
       Container(
           height: keyBoardHeight,
           padding: EdgeInsets.only(left: 24.0, right: 24.0),
-          decoration: BoxDecoration(
-              color: Colors.white,
-              border: Border(
-                  top: BorderSide(width: 1.0, color: Color(0xFFDCDCDC)))),
+          decoration: BoxDecoration(color: Colors.white, border: Border(top: BorderSide(width: 1.0, color: Color(0xFFDCDCDC)))),
           child: GridView(
               shrinkWrap: true,
               padding: EdgeInsets.only(top: 24.0),
@@ -375,16 +358,14 @@ class _MenuBarState extends State<MenuBar>
 //            ],
 //          ),
           Container(
-              padding: const EdgeInsets.symmetric(
-                  vertical: 8.0, horizontal: 12.0),
+              padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
               decoration: shadowTop(),
               child: Row(
                 children: <Widget>[
                   GestureDetector(
                     onTap: () async {
                       if (_focusNode.hasFocus) {
-                        await SystemChannels.textInput
-                            .invokeMethod('TextInput.hide');
+                        await SystemChannels.textInput.invokeMethod('TextInput.hide');
                         setState(() {
                           isShowMenuBottomIndex = 2;
                         });
@@ -396,8 +377,7 @@ class _MenuBarState extends State<MenuBar>
                     },
                     child: Padding(
 //                padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
-                      child: Image.asset(
-                          "lib/assets/img/bbs_icon_addmore.png"),
+                      child: Image.asset("lib/assets/img/bbs_icon_addmore.png"),
                       padding: EdgeInsets.only(right: 12.0),
                     ),
                   ),
@@ -407,20 +387,18 @@ class _MenuBarState extends State<MenuBar>
                         isShowMenuBottomIndex = 3;
                         FocusScope.of(context).requestFocus(_focusNode);
                         if (_focusNode.hasFocus) {
-                          SystemChannels.textInput
-                              .invokeMethod('TextInput.hide');
+                          SystemChannels.textInput.invokeMethod('TextInput.hide');
                         }
                       });
                     },
                     child: Padding(
 //                padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 12.0),
-                      child: Image.asset(
-                          "lib/assets/img/bbs_icon_expression.png"),
+                      child: Image.asset("lib/assets/img/bbs_icon_expression.png"),
                       padding: EdgeInsets.only(right: 12.0),
                     ),
                   ),
                   Expanded(
-                    child: TextField(
+                    child: CupertinoTextField(
                       controller: _controller,
                       focusNode: _focusNode,
                       keyboardType: TextInputType.multiline,
@@ -432,9 +410,9 @@ class _MenuBarState extends State<MenuBar>
                       maxLines: 3,
 //                              maxLength: 200,
                       onChanged: (value) {
-                        setState(() {
-//                                  _textFieldValue = value;
-                        });
+//                         setState(() {
+// //                                  _textFieldValue = value;
+//                         });
                       },
                       onTap: () {
                         setState(() {
@@ -442,65 +420,40 @@ class _MenuBarState extends State<MenuBar>
                         });
                         widget.scrollToBottom();
                       },
-                      decoration: InputDecoration(
-                        filled: true,
-                        fillColor: Color(0xfff1f1f1),
-                        counterText: "",
-                        hintText: "${widget.inputField}",
-                        contentPadding:
-                        EdgeInsets.symmetric(horizontal: 10.0, vertical: 0.0),
-                        border: OutlineInputBorder(
-                          borderSide:
-                          BorderSide(color: Color(0xfff1f1f1), width: 0.5),
-                          borderRadius: BorderRadius.all(Radius.circular(44.0)),
-                        ),
-                        focusedBorder: OutlineInputBorder(
-                          borderSide:
-                          BorderSide(color: Color(0xfff1f1f1), width: 1.0),
-                          borderRadius: BorderRadius.all(Radius.circular(10.0)),
-                        ),
-                        enabledBorder: OutlineInputBorder(
-                          borderSide:
-                          BorderSide(color: Color(0xfff1f1f1), width: 0.5),
-                          borderRadius: BorderRadius.all(Radius.circular(10.0)),
-                        ),
-                      ),
+                      decoration: BoxDecoration(shape: BoxShape.rectangle, borderRadius: BorderRadius.all(Radius.circular(10)), color: Color(0xfff1f1f1)),
                     ),
                   ),
                   Space(
                     width: 5.0,
                   ),
-                  PrimaryButton(
-                    width: 75,
-                    height: 35.0,
-                    content: "发送",
-                    callback: () async {
-                      if (_controller.text.length <= 0) {
-                        ToastUtil.show("请输入正确的内容");
-                        return;
-                      }
-
-                      if (widget.menuIdentity.menuScene == "chat") {
-                        MessageInstance message = (await api.postChatSend(
-                            widget.menuIdentity.userId,
-                            "text",
-                            '{"text":"${_controller.text}"}'))
-                            .data;
-
-                        await add(
-                            message); // await 是等待的标志 我等待完 在做后面的init 的事?
-                        _controller.text = "";
-                      }
-                      if (widget.menuIdentity.menuScene == "feedback") {
-                        await _postFeedBackpostFeedBack(_textFieldValue);
-                      }
-
-                      // 这里可能传不了 callback 所有需要的值都在menu bar里面 只能通过传 不同的 场景 进行 不同的操作
-                    },
-                    shadow: _controller.text.length <= 0 ? false : true,
-                    buttonColor:
-                    _controller.text == "" ? Color(0xffd2d2d2) : null,
-                  ),
+                  ValueListenableBuilder(
+                      valueListenable: _postable,
+                      builder: (_, able, __) => PrimaryButton(
+                            width: 75,
+                            height: 35.0,
+                            content: "发送",
+                            callback: () async {
+                              if (_controller.text.length <= 0) {
+                                ToastUtil.show("请输入正确的内容");
+                                return;
+                              }
+
+                              if (widget.menuIdentity.menuScene == "chat") {
+                                MessageInstance message = (await api.postChatSend(widget.menuIdentity.userId, "text", '{"text":"${_controller.text}"}')).data;
+
+                                await add(message); // await 是等待的标志 我等待完 在做后面的init 的事?
+                                _controller.text = "";
+                              }
+                              if (widget.menuIdentity.menuScene == "feedback") {
+                                await _postFeedBackpostFeedBack(_textFieldValue);
+                              }
+
+                              // 这里可能传不了 callback 所有需要的值都在menu bar里面 只能通过传 不同的 场景 进行 不同的操作
+                            },
+                            shadow: able == true,
+//                            buttonColor: able == false  ? Color(0xffd2d2d2) : null,
+                              buttonColor: able == false ? Color(0xffFFC400).withOpacity(0.3) : null,
+                      )),
                 ],
               )),
         // 底部的骚操作

+ 8 - 6
lib/widgets/misc.dart

@@ -47,7 +47,7 @@ Widget buildLabelWidget(BuildContext context, String title) {
       padding: EdgeInsets.fromLTRB(ui_padding, 10.0, ui_padding, 10.0),
       child: Text(
         title,
-        style: Theme.of(context).textTheme.headline1,
+        style: Theme.of(context).textTheme.headline1.copyWith(fontSize: 16.0,),
       ));
 }
 
@@ -117,7 +117,8 @@ Widget achievementWidget(BuildContext context, Achievement item, {double w = 70,
             ),
             Text(
               item.seriesName  != null ? item.seriesName : item.name,
-              style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: w < 80 ? 12 : 14),
+//              style: Theme.of(context).textTheme.subtitle1.copyWith(fontSize: w < 80 ? 12 : 14),
+            style:Theme.of(context).textTheme.subtitle1.copyWith(fontSize: 12)
             )
           ],
         ),
@@ -216,8 +217,9 @@ header.ClassicalHeader buildClassicalHeader(
     {double extent = 80.0,
     double triggerDistance = 90.0,
     Color infoColor = REFRESH_INFO_COLOR,
-    Color textColor =const Color(0xffFFC400),
-    Color bgColor = Colors.transparent}) {
+    Color textColor = REFRESH_INFO_COLOR,
+    Color bgColor = Colors.transparent,
+    }) {
   return header.ClassicalHeader(
     extent: extent,
     triggerDistance: triggerDistance,
@@ -227,9 +229,9 @@ header.ClassicalHeader buildClassicalHeader(
     refreshedText: '刷新完成',
     refreshingText: '正在刷新...',
     refreshReadyText: '释放刷新',
-    infoColor: Color(0xffFFC400),
+    infoColor: infoColor,
     bgColor: bgColor,
-    textColor: textColor,
+    textColor: infoColor,
   );
 }
 

+ 4 - 4
lib/widgets/persistent_header.dart

@@ -1,9 +1,9 @@
 import 'package:flutter/material.dart';
 
 class PersistentHeader extends SliverPersistentHeaderDelegate {
-  double max;
-  double min;
-  Widget child;
+  final double max;
+  final double min;
+  final Widget child;
 
   PersistentHeader({this.max = 50.0, this.min = 50.0, @required this.child});
 
@@ -19,5 +19,5 @@ class PersistentHeader extends SliverPersistentHeaderDelegate {
   double get minExtent => min;
 
   @override
-  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => true;
+  bool shouldRebuild(SliverPersistentHeaderDelegate oldDelegate) => oldDelegate != this;
 }

+ 1 - 2
lib/widgets/refresh_header.dart

@@ -138,7 +138,6 @@ class ClassicalHeaderWidget extends StatefulWidget {
 
 class ClassicalHeaderWidgetState extends State<ClassicalHeaderWidget> with TickerProviderStateMixin<ClassicalHeaderWidget> {
   static Image _loading = Image.asset("lib/assets/img/image_refresh.png");
-  static Image _done = Image.asset("lib/assets/img/image_refresh_complete.png");
 
   // 是否到达触发刷新距离
   bool _overTriggerDistance = false;
@@ -414,7 +413,7 @@ class ClassicalHeaderWidgetState extends State<ClassicalHeaderWidget> with Ticke
                               widget.refreshState == RefreshMode.done ||
                               (widget.enableInfiniteRefresh && widget.refreshState != RefreshMode.refreshed) ||
                               widget.noMore
-                          ? _done
+                          ? _loading
                           : Transform.rotate(
                               child: _loading,
                               angle: 2 * pi * (widget.pulledExtent / widget.refreshTriggerPullDistance),

+ 76 - 18
lib/widgets/text_input.dart

@@ -21,7 +21,15 @@ class TextInput extends StatefulWidget {
   final bool autoFocus;
   final bool atUser;
 
-  TextInput(this.subjectId, {this.focusNode, this.parentCommentId, this.toCommentId, this.callback, this.comment = false, this.user, this.autoFocus = false, this.atUser = true});
+  TextInput(this.subjectId,
+      {this.focusNode,
+      this.parentCommentId,
+      this.toCommentId,
+      this.callback,
+      this.comment = false,
+      this.user,
+      this.autoFocus = false,
+      this.atUser = true});
 
   @override
   State<StatefulWidget> createState() {
@@ -34,12 +42,17 @@ class _TextInputState extends State<TextInput> with InjectApi {
   TextEditingController _controller;
   FocusNode _focusNode;
   var _posting = false;
+  ValueNotifier<bool> _postable = ValueNotifier(false);
 
   @override
   void initState() {
     super.initState();
     _focusNode = widget.focusNode ?? FocusNode();
-    _controller = TextEditingController();
+    _controller = TextEditingController()
+      ..addListener(() {
+        _postable.value = _controller.value.text.isNotEmpty;
+      });
+
     if (widget.comment) {
       WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
         FocusScope.of(context).requestFocus(_focusNode);
@@ -82,14 +95,16 @@ class _TextInputState extends State<TextInput> with InjectApi {
                     decoration: InputDecoration(
                         counterText: "",
                         hintText: "发表你的看法...",
-                        hintStyle: TextStyle(color: Color(0xff999999),fontSize: 15.0),
+                        hintStyle:
+                            TextStyle(color: Color(0xff999999), fontSize: 15.0),
                         prefixIconConstraints: BoxConstraints(minWidth: 36),
                         prefixIcon: Padding(
-                          padding: const EdgeInsets.fromLTRB(12.0 ,0,0,0),
+                          padding: const EdgeInsets.fromLTRB(12.0, 0, 0, 0),
                           child: Row(
                             mainAxisSize: MainAxisSize.min,
                             children: <Widget>[
-                              Image.asset("lib/assets/img/bbs_icon_reportx.png"),
+                              Image.asset(
+                                  "lib/assets/img/bbs_icon_reportx.png"),
                               SizedBox(
                                 width: 4,
                               ),
@@ -117,21 +132,58 @@ class _TextInputState extends State<TextInput> with InjectApi {
                         width: 22,
                         child: CircularProgressIndicator(),
                       )
-                    : PrimaryButton(
-                        width: 80,
-                        height: 35,
-                        callback: _textFieldValue.isEmpty
-                            ? null
-                            : () async {
-                                if (await showBindPhoneDialog(context) != true) {
+                    :
+//                PrimaryButton(
+//                        width: 80,
+//                        height: 35,
+//                        callback: _textFieldValue.isEmpty
+//                            ? null
+//                            : () async {
+//                                if (await showBindPhoneDialog(context) != true) {
+//                                  return;
+//                                }
+//                                setState(() {
+//                                  _posting = true;
+//                                });
+//                                var resp = await api
+//                                    .postForumComment(widget.subjectId, _textFieldValue,
+//                                        parentCommentId: widget.parentCommentId, toCommentId: widget.toCommentId)
+//                                    .catchError((e) {});
+//                                if (resp != null && resp.code == 0) {
+//                                  ToastUtil.show("发表成功");
+//                                  _controller?.clear();
+//                                  _focusNode?.unfocus();
+//                                  widget.callback?.call();
+//                                  _textFieldValue = '';
+//                                  CommentNotification(Comment(id: resp.data.commentId, subjectId: widget.subjectId), CommentNotification.TYPE_ADD)
+//                                      .dispatch(context);
+//                                }
+//                                setState(() {
+//                                  _posting = false;
+//                                });
+////                      SystemChannels.textInput.invokeMethod('TextInput.hide');
+//                              },
+//                        content: "发表",
+//                      ),
+                    ValueListenableBuilder(
+                        valueListenable: _postable,
+                        builder: (_, able, __) => PrimaryButton(
+                              content: "发表",
+                              width: 80,
+                              height: 35,
+                              callback: () async {
+                                if (await showBindPhoneDialog(context) !=
+                                    true) {
                                   return;
                                 }
                                 setState(() {
                                   _posting = true;
                                 });
                                 var resp = await api
-                                    .postForumComment(widget.subjectId, _textFieldValue,
-                                        parentCommentId: widget.parentCommentId, toCommentId: widget.toCommentId)
+                                    .postForumComment(
+                                        widget.subjectId, _textFieldValue,
+                                        parentCommentId: widget.parentCommentId,
+                                        toCommentId: widget.toCommentId)
                                     .catchError((e) {});
                                 if (resp != null && resp.code == 0) {
                                   ToastUtil.show("发表成功");
@@ -139,16 +191,22 @@ class _TextInputState extends State<TextInput> with InjectApi {
                                   _focusNode?.unfocus();
                                   widget.callback?.call();
                                   _textFieldValue = '';
-                                  CommentNotification(Comment(id: resp.data.commentId, subjectId: widget.subjectId), CommentNotification.TYPE_ADD)
+                                  CommentNotification(
+                                          Comment(
+                                              id: resp.data.commentId,
+                                              subjectId: widget.subjectId),
+                                          CommentNotification.TYPE_ADD)
                                       .dispatch(context);
                                 }
                                 setState(() {
                                   _posting = false;
                                 });
-//                      SystemChannels.textInput.invokeMethod('TextInput.hide');
                               },
-                        content: "发表",
-                      ),
+                              shadow: able == true,
+                              buttonColor: able == false
+                                  ? Color(0xffFFC400).withOpacity(0.3)
+                                  : null,
+                            )),
               )
             ],
           ),

+ 3 - 0
plugin/qrscanner/CHANGELOG.md

@@ -0,0 +1,3 @@
+## 0.0.1
+
+* TODO: Describe initial release.

+ 21 - 0
plugin/qrscanner/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 alvagan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 43 - 0
plugin/qrscanner/README.md

@@ -0,0 +1,43 @@
+# flutter_plugin_qr_scanner
+
+A QR code scanner flutter plugin project. Support iOS, Android.
+
+## Getting Started
+``` dart
+import 'package:flutter_plugin_qr_scanner/qrscanner.dart';
+
+Future<void> initCodeState() async {
+  String code;
+  // Platform messages may fail, so we use a try/catch PlatformException.
+  try {
+    code = await QrScanner.scan(
+      title: "scanner",
+      laserColor: Colors.white, //default #ffff55ff
+      playBeep: true, //default false
+      promptMessage: "Point the QR code to the frame to complete the scan.",
+      errorMsg: "Oops, something went wrong. You may need to check your permission of camera or restart the device.",
+      permissionDeniedText: "Your privacy settings seem to prevent us from accessing your camera for barcode scanning. You can fix it by doing this, touch the OK button below to open the Settings and then turn the Camera on.",
+      messageConfirmText: "OK",
+      messageCancelText: "Cancel",
+    );
+  } on PlatformException {
+    code = 'Failed to get qr code.';
+  }
+
+  // If the widget was removed from the tree while the asynchronous platform
+  // message was in flight, we want to discard the reply rather than calling
+  // setState to update our non-existent appearance.
+  if (!mounted) return;
+
+  setState(() {
+    _code = code;
+  });
+}
+```
+
+## For IOS
+You will need provide the description of camera's permission to work properly, otherwise will crash your app.
+```
+  <key>NSCameraUsageDescription</key>
+	<string>The purpose that you use the camera</string>
+```

+ 45 - 0
plugin/qrscanner/android/build.gradle

@@ -0,0 +1,45 @@
+group 'com.alva.flutter.plugin.scanner'
+version '1.0-SNAPSHOT'
+
+buildscript {
+    ext.kotlin_version = '1.3.50'
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.5.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+}
+
+rootProject.allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+    compileSdkVersion 28
+
+    sourceSets {
+        main.java.srcDirs += 'src/main/kotlin'
+    }
+    defaultConfig {
+        minSdkVersion 16
+    }
+    lintOptions {
+        disable 'InvalidPackage'
+    }
+}
+
+dependencies {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+    implementation('com.google.zxing:core:3.3.3')
+}

+ 4 - 0
plugin/qrscanner/android/gradle.properties

@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true

+ 5 - 0
plugin/qrscanner/android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+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

+ 2 - 0
plugin/qrscanner/android/local.properties

@@ -0,0 +1,2 @@
+sdk.dir=/Users/duowan123/Library/Android/sdk
+flutter.sdk=/Users/duowan123/Downloads/flutter

+ 1 - 0
plugin/qrscanner/android/settings.gradle

@@ -0,0 +1 @@
+rootProject.name = 'qrscanner'

+ 16 - 0
plugin/qrscanner/android/src/main/AndroidManifest.xml

@@ -0,0 +1,16 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+  package="com.alva.flutter.plugin.scanner">
+  <uses-permission android:name="android.permission.CAMERA" />
+  <uses-permission android:name="android.permission.VIBRATE" />
+  <uses-permission android:name="android.permission.FLASHLIGHT" />
+  <uses-feature android:name="android.hardware.camera" />
+  <uses-feature android:name="android.hardware.camera.autofocus" />
+  <uses-feature android:name="android.hardware.camera.flash" />
+  <application>
+    <activity
+        android:launchMode="singleTop"
+        android:theme="@style/ScannerTheme"
+        android:configChanges="keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+        android:name="com.google.zxing.client.android.CaptureActivity" />
+  </application>
+</manifest>

+ 77 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/AmbientLightManager.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2012 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.preference.PreferenceManager;
+import com.google.zxing.client.android.camera.CameraManager;
+
+/**
+ * Detects ambient light and switches on the front light when very dark, and off again when sufficiently light.
+ *
+ * @author Sean Owen
+ * @author Nikolaus Huber
+ */
+final class AmbientLightManager implements SensorEventListener {
+
+  private static final float TOO_DARK_LUX = 45.0f;
+  private static final float BRIGHT_ENOUGH_LUX = 450.0f;
+
+  private final Context context;
+  private CameraManager cameraManager;
+  private Sensor lightSensor;
+
+  AmbientLightManager(Context context) {
+    this.context = context;
+  }
+
+  void start(CameraManager cameraManager) {
+    this.cameraManager = cameraManager;
+  }
+
+  void stop() {
+    if (lightSensor != null) {
+      SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
+      sensorManager.unregisterListener(this);
+      cameraManager = null;
+      lightSensor = null;
+    }
+  }
+
+  @Override
+  public void onSensorChanged(SensorEvent sensorEvent) {
+    float ambientLightLux = sensorEvent.values[0];
+    if (cameraManager != null) {
+      if (ambientLightLux <= TOO_DARK_LUX) {
+        cameraManager.setTorch(true);
+      } else if (ambientLightLux >= BRIGHT_ENOUGH_LUX) {
+        cameraManager.setTorch(false);
+      }
+    }
+  }
+
+  @Override
+  public void onAccuracyChanged(Sensor sensor, int accuracy) {
+    // do nothing
+  }
+
+}

+ 112 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/BeepManager.java

@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2010 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.res.AssetFileDescriptor;
+import android.media.AudioManager;
+import android.media.MediaPlayer;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+import com.alva.flutter.plugin.scanner.R;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * Manages beeps and vibrations for {@link CaptureActivity}.
+ */
+final class BeepManager implements MediaPlayer.OnErrorListener, Closeable {
+
+  private static final String TAG = BeepManager.class.getSimpleName();
+
+  private static final float BEEP_VOLUME = 0.6f;
+
+  private final Activity activity;
+  private MediaPlayer mediaPlayer;
+  private boolean playBeep;
+
+  BeepManager(Activity activity, boolean playBeep) {
+    this.activity = activity;
+    this.mediaPlayer = null;
+    this.playBeep = playBeep;
+    updatePrefs();
+  }
+
+  synchronized void updatePrefs() {
+    if (playBeep && mediaPlayer == null) {
+      // The volume on STREAM_SYSTEM is not adjustable, and users found it too loud,
+      // so we now play on the music stream.
+      activity.setVolumeControlStream(AudioManager.STREAM_MUSIC);
+      mediaPlayer = buildMediaPlayer(activity);
+    }
+  }
+
+  synchronized void playBeepSoundAndVibrate() {
+    if (playBeep && mediaPlayer != null) {
+      mediaPlayer.start();
+    }
+  }
+
+  private MediaPlayer buildMediaPlayer(Context activity) {
+    MediaPlayer mediaPlayer = new MediaPlayer();
+    try {
+      if(Build.VERSION.SDK_INT>Build.VERSION_CODES.JELLY_BEAN_MR2){
+        AssetFileDescriptor file = activity.getResources().openRawResourceFd(R.raw.beep);
+        mediaPlayer.setDataSource(file.getFileDescriptor(), file.getStartOffset(), file.getLength());
+        mediaPlayer.prepare();
+      }else{
+        mediaPlayer = MediaPlayer.create(activity, R.raw.beep);
+      }
+      mediaPlayer.setOnErrorListener(this);
+      mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
+      mediaPlayer.setLooping(false);
+      mediaPlayer.setVolume(BEEP_VOLUME, BEEP_VOLUME);
+      return mediaPlayer;
+    } catch (IOException ioe) {
+      Log.w(TAG, ioe);
+      mediaPlayer.release();
+      return null;
+    }
+  }
+
+  @Override
+  public synchronized boolean onError(MediaPlayer mp, int what, int extra) {
+    if (what == MediaPlayer.MEDIA_ERROR_SERVER_DIED) {
+      // we are finished, so put up an appropriate error toast if required and finish
+      activity.finish();
+    } else {
+      // possibly media player error, so release and recreate
+      close();
+      updatePrefs();
+    }
+    return true;
+  }
+
+  @Override
+  public synchronized void close() {
+    if (mediaPlayer != null) {
+      mediaPlayer.release();
+      mediaPlayer = null;
+    }
+  }
+
+}

+ 426 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/CaptureActivity.java

@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2008 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.Result;
+import com.google.zxing.client.android.camera.CameraManager;
+import com.alva.flutter.plugin.scanner.R;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * This activity opens the camera and does the actual scanning on a background thread. It draws a
+ * viewfinder to help the user place the barcode correctly, shows feedback as the image processing
+ * is happening, and then overlays the results when a scan is successful.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ * @author Sean Owen
+ */
+public final class CaptureActivity extends Activity implements SurfaceHolder.Callback {
+
+  private static final String TAG = CaptureActivity.class.getSimpleName();
+  private static final int REQUEST_CODE_ASK_CAMERA = 10;
+
+  private CameraManager cameraManager;
+  private CaptureActivityHandler handler;
+  private ViewfinderView viewfinderView;
+  private boolean hasSurface;
+  private Collection<BarcodeFormat> decodeFormats;
+  private Map<DecodeHintType,?> decodeHints;
+  private String characterSet;
+  private InactivityTimer inactivityTimer;
+  private BeepManager beepManager;
+  private AmbientLightManager ambientLightManager;
+
+  private String errorMsg;
+  private String permissionDeniedMessage;
+  private String msgConfirmText;
+  private String msgCancelText;
+
+  public Handler getHandler() {
+    return handler;
+  }
+
+  CameraManager getCameraManager() {
+    return cameraManager;
+  }
+
+  @Override
+  public void onCreate(Bundle icicle) {
+    super.onCreate(icicle);
+
+    Window window = getWindow();
+    window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+      window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+      window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+      window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
+      window.setStatusBarColor(Color.TRANSPARENT);
+      getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+      window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+      window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
+    }
+    setContentView(R.layout.capture);
+
+    errorMsg = getString(R.string.scanner_msg_framework_bug);
+    permissionDeniedMessage = getString(R.string.scanner_msg_permission_denied);
+    msgConfirmText = getString(R.string.scanner_button_ok);
+    msgCancelText = getString(R.string.scanner_button_cancel);
+
+    hasSurface = false;
+    inactivityTimer = new InactivityTimer(this);
+    beepManager = new BeepManager(this, getIntent().getBooleanExtra(Intents.Scan.KEY_PLAY_BEEP, false));
+    ambientLightManager = new AmbientLightManager(this);
+
+    Intent intent = getIntent();
+    if (intent != null) {
+      if (intent.hasExtra(Intents.Scan.ERROR_MESSAGE)) {
+        errorMsg = intent.getStringExtra(Intents.Scan.ERROR_MESSAGE);
+      }
+
+      if (intent.hasExtra(Intents.Scan.PERMISSION_DENIED_MESSAGE)) {
+        permissionDeniedMessage = intent.getStringExtra(Intents.Scan.PERMISSION_DENIED_MESSAGE);
+      }
+
+      if (intent.hasExtra(Intents.Scan.MESSAGE_CONFIRM_TEXT)) {
+        msgConfirmText = intent.getStringExtra(Intents.Scan.MESSAGE_CONFIRM_TEXT);
+      }
+
+      if (intent.hasExtra(Intents.Scan.MESSAGE_CANCEL_TEXT)) {
+        msgCancelText = intent.getStringExtra(Intents.Scan.MESSAGE_CANCEL_TEXT);
+      }
+    }
+
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
+        ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
+      ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA},
+          REQUEST_CODE_ASK_CAMERA);
+    }
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    initial();
+  }
+
+  @SuppressLint("SourceLockedOrientationActivity")
+  private void initial(){
+    // CameraManager must be initialized here, not in onCreate(). This is necessary because we don't
+    // want to open the camera driver and measure the screen size if we're going to show the help on
+    // first launch. That led to bugs where the scanning rectangle was the wrong size and partially
+    // off screen.
+    cameraManager = new CameraManager(getApplication());
+
+    viewfinderView = findViewById(R.id.viewfinder_view);
+    viewfinderView.setCameraManager(cameraManager, getIntent().getIntExtra(Intents.Scan.LASER_COLOR, 0));
+
+    TextView statusView = findViewById(R.id.status_view);
+    statusView.setText(getResources().getString(R.string.scanner_msg_status));
+
+    handler = null;
+
+    setRequestedOrientation(getCurrentOrientation());
+
+    beepManager.updatePrefs();
+    ambientLightManager.start(cameraManager);
+
+    inactivityTimer.onResume();
+
+    Intent intent = getIntent();
+
+    decodeFormats = null;
+    characterSet = null;
+
+    if (intent != null) {
+
+      String action = intent.getAction();
+      String dataString = intent.getDataString();
+
+      ((TextView) findViewById(R.id.title)).setText(intent.getStringExtra(Intents.Scan.SCAN_TITLE));
+      if (Intents.Scan.ACTION.equals(action)) {
+
+        // Scan the formats the intent requested, and return the result to the calling activity.
+        decodeFormats = DecodeFormatManager.parseDecodeFormats(intent);
+        decodeHints = DecodeHintManager.parseDecodeHints(intent);
+
+        if (intent.hasExtra(Intents.Scan.WIDTH) && intent.hasExtra(Intents.Scan.HEIGHT)) {
+          int width = intent.getIntExtra(Intents.Scan.WIDTH, 0);
+          int height = intent.getIntExtra(Intents.Scan.HEIGHT, 0);
+          if (width > 0 && height > 0) {
+            cameraManager.setManualFramingRect(width, height);
+          }
+        }
+
+        if (intent.hasExtra(Intents.Scan.CAMERA_ID)) {
+          int cameraId = intent.getIntExtra(Intents.Scan.CAMERA_ID, -1);
+          if (cameraId >= 0) {
+            cameraManager.setManualCameraId(cameraId);
+          }
+        }
+
+        String customPromptMessage = intent.getStringExtra(Intents.Scan.PROMPT_MESSAGE);
+        if (customPromptMessage != null) {
+          statusView.setText(customPromptMessage);
+        }
+
+      } else if (dataString != null) {
+        decodeFormats = DecodeFormatManager.PRODUCT_FORMATS;
+      }
+      characterSet = intent.getStringExtra(Intents.Scan.CHARACTER_SET);
+    }
+
+    SurfaceView surfaceView = findViewById(R.id.preview_view);
+    SurfaceHolder surfaceHolder = surfaceView.getHolder();
+    if (hasSurface) {
+      // The activity was paused but not stopped, so the surface still exists. Therefore
+      // surfaceCreated() won't be called, so init the camera here.
+      initCamera(surfaceHolder);
+    } else {
+      // Install the callback and wait for surfaceCreated() to init the camera.
+      surfaceHolder.addCallback(this);
+    }
+  }
+
+  private int getCurrentOrientation() {
+    int rotation = getWindowManager().getDefaultDisplay().getRotation();
+    if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
+      switch (rotation) {
+        case Surface.ROTATION_0:
+        case Surface.ROTATION_90:
+          return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+        case Surface.ROTATION_270:
+        case Surface.ROTATION_180:
+        default:
+          return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+      }
+    } else {
+      switch (rotation) {
+        case Surface.ROTATION_0:
+        case Surface.ROTATION_270:
+          return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+        case Surface.ROTATION_90:
+        case Surface.ROTATION_180:
+        default:
+          return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+      }
+    }
+  }
+
+  @Override
+  protected void onPause() {
+    if (handler != null) {
+      handler.quitSynchronously();
+      handler = null;
+    }
+    inactivityTimer.onPause();
+    ambientLightManager.stop();
+    beepManager.close();
+    if(cameraManager != null)
+      cameraManager.closeDriver();
+    //historyManager = null; // Keep for onActivityResult
+    if (!hasSurface) {
+      SurfaceView surfaceView = findViewById(R.id.preview_view);
+      SurfaceHolder surfaceHolder = surfaceView.getHolder();
+      surfaceHolder.removeCallback(this);
+    }
+    super.onPause();
+  }
+
+  @Override
+  protected void onDestroy() {
+    inactivityTimer.shutdown();
+    super.onDestroy();
+  }
+
+  @Override
+  public boolean onKeyDown(int keyCode, KeyEvent event) {
+    switch (keyCode) {
+      case KeyEvent.KEYCODE_BACK:
+        setResult(RESULT_CANCELED);
+        finish();
+        break;
+      case KeyEvent.KEYCODE_FOCUS:
+      case KeyEvent.KEYCODE_CAMERA:
+        // Handle these events so they don't launch the Camera app
+        return true;
+      // Use volume up/down to turn on light
+      case KeyEvent.KEYCODE_VOLUME_DOWN:
+        cameraManager.setTorch(false);
+        return true;
+      case KeyEvent.KEYCODE_VOLUME_UP:
+        cameraManager.setTorch(true);
+        return true;
+    }
+    return super.onKeyDown(keyCode, event);
+  }
+
+  @Override
+  public void surfaceCreated(SurfaceHolder holder) {
+    if (holder == null) {
+      Log.e(TAG, "*** WARNING *** surfaceCreated() gave us a null surface!");
+    }
+    if (!hasSurface) {
+      hasSurface = true;
+      initCamera(holder);
+    }
+  }
+
+  @Override
+  public void surfaceDestroyed(SurfaceHolder holder) {
+    hasSurface = false;
+  }
+
+  @Override
+  public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+    // do nothing
+  }
+
+  /**
+   * A valid barcode has been found, so give an indication of success and show the results.
+   *
+   * @param rawResult The contents of the barcode.
+   */
+  public void handleDecode(Result rawResult) {
+    inactivityTimer.onActivity();
+
+    boolean fromLiveScan = rawResult != null;
+    if (fromLiveScan) {
+      // Then not from history, so beep/vibrate and we have an image to draw on
+      beepManager.playBeepSoundAndVibrate();
+      Intent intent = new Intent();
+      intent.putExtra(Intent.EXTRA_TEXT, rawResult.getText());
+      intent.putExtra(Intent.EXTRA_SUBJECT, rawResult.getRawBytes());
+      setResult(RESULT_OK, intent);
+      finish();
+    }
+  }
+
+  private void initCamera(SurfaceHolder surfaceHolder) {
+    if (surfaceHolder == null) {
+      throw new IllegalStateException("No SurfaceHolder provided");
+    }
+    if (cameraManager.isOpen()) {
+      Log.w(TAG, "initCamera() while already open -- late SurfaceView callback?");
+      return;
+    }
+    try {
+      cameraManager.openDriver(surfaceHolder);
+      // Creating the handler starts the preview, which can also throw a RuntimeException.
+      if (handler == null) {
+        handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager);
+      }
+    } catch (IOException ioe) {
+      Log.w(TAG, ioe);
+      displayFrameworkBugMessageAndExit();
+    } catch (RuntimeException e) {
+      // Barcode Scanner has seen crashes in the wild of this variety:
+      // java.?lang.?RuntimeException: Fail to connect to camera service
+      Log.w(TAG, "Unexpected error initializing camera", e);
+      displayFrameworkBugMessageAndExit();
+    }
+  }
+
+  private void displayFrameworkBugMessageAndExit() {
+    AlertDialog.Builder builder = new AlertDialog.Builder(this);
+    builder.setMessage(errorMsg);
+    builder.setPositiveButton(msgConfirmText, new FinishListener(this));
+    builder.setOnCancelListener(new FinishListener(this));
+    builder.show();
+  }
+
+  public void drawViewfinder() {
+    viewfinderView.drawViewfinder();
+  }
+
+  public void onButtonClick(View view) {
+    setResult(RESULT_CANCELED);
+    finish();
+  }
+
+  @Override
+  public void onRequestPermissionsResult(int requestCode, @NotNull String[] permissions, @NotNull int[] grantResults) {
+    if (requestCode == REQUEST_CODE_ASK_CAMERA) {
+      for (int result : grantResults) {
+        if (result != PackageManager.PERMISSION_GRANTED){
+          AlertDialog.Builder builder = new AlertDialog.Builder(this)
+            .setMessage(permissionDeniedMessage)
+            .setPositiveButton(msgConfirmText, new DialogInterface.OnClickListener() {
+              @Override
+              public void onClick(DialogInterface dialogInterface, int i) {
+                dialogInterface.dismiss();
+                Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+                Uri uri = Uri.fromParts("package", getPackageName(), null);
+                intent.setData(uri);
+                startActivity(intent);
+                setResult(RESULT_CANCELED);
+                finish();
+              }
+            })
+            .setNegativeButton(msgCancelText, new DialogInterface.OnClickListener() {
+              @Override
+              public void onClick(DialogInterface dialogInterface, int i) {
+                dialogInterface.dismiss();
+                setResult(RESULT_CANCELED);
+                finish();
+              }
+            });
+          builder.create().show();
+          return;
+        }
+      }
+      hasSurface = false;
+      initial();
+    }
+  }
+}

+ 163 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/CaptureActivityHandler.java

@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2008 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android;
+
+import android.content.ActivityNotFoundException;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.provider.Browser;
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.Result;
+import com.google.zxing.client.android.camera.CameraManager;
+import com.alva.flutter.plugin.scanner.R;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.Collection;
+import java.util.Map;
+
+/**
+ * This class handles all the messaging which comprises the state machine for capture.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class CaptureActivityHandler extends Handler {
+
+  private static final String TAG = CaptureActivityHandler.class.getSimpleName();
+
+  private final CaptureActivity activity;
+  private final DecodeThread decodeThread;
+  private State state;
+  private final CameraManager cameraManager;
+
+  private static final int restart_preview = 0x01;
+  public static final int decode_succeeded = 0x02;
+  public static final int decode_failed = 0x03;
+  private static final int return_scan_result = 0x04;
+  private static final int launch_product_query = 0x05;
+  public static final int decode = 0x06;
+  public static final int quit_code = 0x07;
+
+  private enum State {
+    PREVIEW,
+    SUCCESS,
+    DONE
+  }
+
+  CaptureActivityHandler(CaptureActivity activity,
+                         Collection<BarcodeFormat> decodeFormats,
+                         Map<DecodeHintType,?> baseHints,
+                         String characterSet,
+                         CameraManager cameraManager) {
+    this.activity = activity;
+    decodeThread = new DecodeThread(activity, decodeFormats, baseHints, characterSet, null);
+    decodeThread.start();
+    state = State.SUCCESS;
+
+    // Start ourselves capturing previews and decoding.
+    this.cameraManager = cameraManager;
+    cameraManager.startPreview();
+    restartPreviewAndDecode();
+  }
+
+  @Override
+  public void handleMessage(Message message) {
+    switch (message.what) {
+      case restart_preview:
+        restartPreviewAndDecode();
+        break;
+      case decode_succeeded:
+        state = State.SUCCESS;
+        activity.handleDecode((Result) message.obj);
+        break;
+      case decode_failed:
+        // We're decoding as fast as possible, so when one decode fails, start another.
+        state = State.PREVIEW;
+        cameraManager.requestPreviewFrame(decodeThread.getHandler(), decode);
+        break;
+      case return_scan_result:
+        activity.setResult(Activity.RESULT_OK, (Intent) message.obj);
+        activity.finish();
+        break;
+      case launch_product_query:
+        String url = (String) message.obj;
+
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.addFlags(Intents.FLAG_NEW_DOC);
+        intent.setData(Uri.parse(url));
+
+        ResolveInfo resolveInfo =
+            activity.getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
+        String browserPackageName = null;
+        if (resolveInfo != null && resolveInfo.activityInfo != null) {
+          browserPackageName = resolveInfo.activityInfo.packageName;
+          Log.d(TAG, "Using browser in package " + browserPackageName);
+        }
+
+        // Needed for default Android browser / Chrome only apparently
+        if (browserPackageName != null) {
+          switch (browserPackageName) {
+            case "com.android.browser":
+            case "com.android.chrome":
+              intent.setPackage(browserPackageName);
+              intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+              intent.putExtra(Browser.EXTRA_APPLICATION_ID, browserPackageName);
+              break;
+          }
+        }
+
+        try {
+          activity.startActivity(intent);
+        } catch (ActivityNotFoundException ignored) {
+          Log.w(TAG, "Can't find anything to handle VIEW of URI");
+        }
+        break;
+    }
+  }
+
+  void quitSynchronously() {
+    state = State.DONE;
+    cameraManager.stopPreview();
+    Message quit = Message.obtain(decodeThread.getHandler(), quit_code);
+    quit.sendToTarget();
+    try {
+      // Wait at most half a second; should be enough time, and onPause() will timeout quickly
+      decodeThread.join(500L);
+    } catch (InterruptedException e) {
+      // continue
+    }
+
+    // Be absolutely sure we don't send any queued up messages
+    removeMessages(decode_succeeded);
+    removeMessages(decode_failed);
+  }
+
+  private void restartPreviewAndDecode() {
+    if (state == State.SUCCESS) {
+      state = State.PREVIEW;
+      cameraManager.requestPreviewFrame(decodeThread.getHandler(), decode);
+      activity.drawViewfinder();
+    }
+  }
+
+}

+ 118 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/Contents.java

@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2008 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android;
+
+import android.provider.ContactsContract;
+
+/**
+ * The set of constants to use when sending Barcode Scanner an Intent which requests a barcode
+ * to be encoded.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class Contents {
+  private Contents() {
+  }
+
+  /**
+   * Contains type constants used when sending Intents.
+   */
+  public static final class Type {
+    /**
+     * Plain text. Use Intent.putExtra(DATA, string). This can be used for URLs too, but string
+     * must include "http://" or "https://".
+     */
+    public static final String TEXT = "TEXT_TYPE";
+
+    /**
+     * An email type. Use Intent.putExtra(DATA, string) where string is the email address.
+     */
+    public static final String EMAIL = "EMAIL_TYPE";
+
+    /**
+     * Use Intent.putExtra(DATA, string) where string is the phone number to call.
+     */
+    public static final String PHONE = "PHONE_TYPE";
+
+    /**
+     * An SMS type. Use Intent.putExtra(DATA, string) where string is the number to SMS.
+     */
+    public static final String SMS = "SMS_TYPE";
+
+    /**
+     * A contact. Send a request to encode it as follows:
+     * {@code
+     * import android.provider.Contacts;
+     *
+     * Intent intent = new Intent(Intents.Encode.ACTION);
+     * intent.putExtra(Intents.Encode.TYPE, CONTACT);
+     * Bundle bundle = new Bundle();
+     * bundle.putString(ContactsContract.Intents.Insert.NAME, "Jenny");
+     * bundle.putString(ContactsContract.Intents.Insert.PHONE, "8675309");
+     * bundle.putString(ContactsContract.Intents.Insert.EMAIL, "jenny@the80s.com");
+     * bundle.putString(ContactsContract.Intents.Insert.POSTAL, "123 Fake St. San Francisco, CA 94102");
+     * intent.putExtra(Intents.Encode.DATA, bundle);
+     * }
+     */
+    public static final String CONTACT = "CONTACT_TYPE";
+
+    /**
+     * A geographic location. Use as follows:
+     * Bundle bundle = new Bundle();
+     * bundle.putFloat("LAT", latitude);
+     * bundle.putFloat("LONG", longitude);
+     * intent.putExtra(Intents.Encode.DATA, bundle);
+     */
+    public static final String LOCATION = "LOCATION_TYPE";
+
+    private Type() {
+    }
+  }
+
+  public static final String URL_KEY = "URL_KEY";
+
+  public static final String NOTE_KEY = "NOTE_KEY";
+
+  /**
+   * When using Type.CONTACT, these arrays provide the keys for adding or retrieving multiple
+   * phone numbers and addresses.
+   */
+  public static final String[] PHONE_KEYS = {
+      ContactsContract.Intents.Insert.PHONE,
+      ContactsContract.Intents.Insert.SECONDARY_PHONE,
+      ContactsContract.Intents.Insert.TERTIARY_PHONE
+  };
+
+  public static final String[] PHONE_TYPE_KEYS = {
+      ContactsContract.Intents.Insert.PHONE_TYPE,
+      ContactsContract.Intents.Insert.SECONDARY_PHONE_TYPE,
+      ContactsContract.Intents.Insert.TERTIARY_PHONE_TYPE
+  };
+
+  public static final String[] EMAIL_KEYS = {
+      ContactsContract.Intents.Insert.EMAIL,
+      ContactsContract.Intents.Insert.SECONDARY_EMAIL,
+      ContactsContract.Intents.Insert.TERTIARY_EMAIL
+  };
+
+  public static final String[] EMAIL_TYPE_KEYS = {
+      ContactsContract.Intents.Insert.EMAIL_TYPE,
+      ContactsContract.Intents.Insert.SECONDARY_EMAIL_TYPE,
+      ContactsContract.Intents.Insert.TERTIARY_EMAIL_TYPE
+  };
+
+}

+ 103 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/DecodeFormatManager.java

@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2010 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android;
+
+import android.content.Intent;
+import com.google.zxing.BarcodeFormat;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+final class DecodeFormatManager {
+
+  private static final Pattern COMMA_PATTERN = Pattern.compile(",");
+
+  static final Set<BarcodeFormat> PRODUCT_FORMATS;
+  static final Set<BarcodeFormat> INDUSTRIAL_FORMATS;
+  private static final Set<BarcodeFormat> ONE_D_FORMATS;
+  static final Set<BarcodeFormat> QR_CODE_FORMATS = EnumSet.of(BarcodeFormat.QR_CODE);
+  static final Set<BarcodeFormat> DATA_MATRIX_FORMATS = EnumSet.of(BarcodeFormat.DATA_MATRIX);
+  static final Set<BarcodeFormat> AZTEC_FORMATS = EnumSet.of(BarcodeFormat.AZTEC);
+  static final Set<BarcodeFormat> PDF417_FORMATS = EnumSet.of(BarcodeFormat.PDF_417);
+  static {
+    PRODUCT_FORMATS = EnumSet.of(BarcodeFormat.UPC_A,
+                                 BarcodeFormat.UPC_E,
+                                 BarcodeFormat.EAN_13,
+                                 BarcodeFormat.EAN_8,
+                                 BarcodeFormat.RSS_14,
+                                 BarcodeFormat.RSS_EXPANDED);
+    INDUSTRIAL_FORMATS = EnumSet.of(BarcodeFormat.CODE_39,
+                                    BarcodeFormat.CODE_93,
+                                    BarcodeFormat.CODE_128,
+                                    BarcodeFormat.ITF,
+                                    BarcodeFormat.CODABAR);
+    ONE_D_FORMATS = EnumSet.copyOf(PRODUCT_FORMATS);
+    ONE_D_FORMATS.addAll(INDUSTRIAL_FORMATS);
+  }
+  private static final Map<String,Set<BarcodeFormat>> FORMATS_FOR_MODE;
+  static {
+    FORMATS_FOR_MODE = new HashMap<>();
+    FORMATS_FOR_MODE.put(Intents.Scan.ONE_D_MODE, ONE_D_FORMATS);
+    FORMATS_FOR_MODE.put(Intents.Scan.PRODUCT_MODE, PRODUCT_FORMATS);
+    FORMATS_FOR_MODE.put(Intents.Scan.QR_CODE_MODE, QR_CODE_FORMATS);
+    FORMATS_FOR_MODE.put(Intents.Scan.DATA_MATRIX_MODE, DATA_MATRIX_FORMATS);
+    FORMATS_FOR_MODE.put(Intents.Scan.AZTEC_MODE, AZTEC_FORMATS);
+    FORMATS_FOR_MODE.put(Intents.Scan.PDF417_MODE, PDF417_FORMATS);
+  }
+
+  private DecodeFormatManager() {}
+
+  static Set<BarcodeFormat> parseDecodeFormats(Intent intent) {
+    Iterable<String> scanFormats = null;
+    CharSequence scanFormatsString = intent.getStringExtra(Intents.Scan.FORMATS);
+    if (scanFormatsString != null) {
+      scanFormats = Arrays.asList(COMMA_PATTERN.split(scanFormatsString));
+    }
+    return parseDecodeFormats(scanFormats, intent.getStringExtra(Intents.Scan.MODE));
+  }
+
+//  static Set<BarcodeFormat> parseDecodeFormats(Uri inputUri) {
+//    List<String> formats = inputUri.getQueryParameters(Intents.Scan.FORMATS);
+//    if (formats != null && formats.size() == 1 && formats.get(0) != null) {
+//      formats = Arrays.asList(COMMA_PATTERN.split(formats.get(0)));
+//    }
+//    return parseDecodeFormats(formats, inputUri.getQueryParameter(Intents.Scan.MODE));
+//  }
+
+  private static Set<BarcodeFormat> parseDecodeFormats(Iterable<String> scanFormats, String decodeMode) {
+    if (scanFormats != null) {
+      Set<BarcodeFormat> formats = EnumSet.noneOf(BarcodeFormat.class);
+      try {
+        for (String format : scanFormats) {
+          formats.add(BarcodeFormat.valueOf(format));
+        }
+        return formats;
+      } catch (IllegalArgumentException iae) {
+        // ignore it then
+      }
+    }
+    if (decodeMode != null) {
+      return FORMATS_FOR_MODE.get(decodeMode);
+    }
+    return null;
+  }
+
+}

+ 107 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/DecodeHandler.java

@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2010 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android;
+
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.MultiFormatReader;
+import com.google.zxing.PlanarYUVLuminanceSource;
+import com.google.zxing.ReaderException;
+import com.google.zxing.Result;
+import com.google.zxing.common.HybridBinarizer;
+import com.alva.flutter.plugin.scanner.R;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+final class DecodeHandler extends Handler {
+
+  private static final String TAG = DecodeHandler.class.getSimpleName();
+
+  private final CaptureActivity activity;
+  private final MultiFormatReader multiFormatReader;
+  private boolean running = true;
+
+  DecodeHandler(CaptureActivity activity, Map<DecodeHintType,Object> hints) {
+    multiFormatReader = new MultiFormatReader();
+    multiFormatReader.setHints(hints);
+    this.activity = activity;
+  }
+
+  @Override
+  public void handleMessage(@NotNull Message message) {
+    if (!running) {
+      return;
+    }
+    switch (message.what) {
+      case CaptureActivityHandler.decode:
+        decode((byte[]) message.obj, message.arg1, message.arg2);
+        break;
+      case CaptureActivityHandler.quit_code:
+        running = false;
+        Looper.myLooper().quit();
+        break;
+    }
+  }
+
+  /**
+   * Decode the data within the viewfinder rectangle, and time how long it took. For efficiency,
+   * reuse the same reader objects from one decode to the next.
+   *
+   * @param data   The YUV preview frame.
+   * @param width  The width of the preview frame.
+   * @param height The height of the preview frame.
+   */
+  private void decode(byte[] data, int width, int height) {
+    long start = System.nanoTime();
+    Result rawResult = null;
+    PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
+    if (source != null) {
+      BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
+      try {
+        rawResult = multiFormatReader.decodeWithState(bitmap);
+      } catch (ReaderException re) {
+        // continue
+      } finally {
+        multiFormatReader.reset();
+      }
+    }
+
+    Handler handler = activity.getHandler();
+    if (rawResult != null) {
+      // Don't log the barcode contents for security.
+      long end = System.nanoTime();
+      Log.d(TAG, "Found barcode in " + TimeUnit.NANOSECONDS.toMillis(end - start) + " ms");
+      if (handler != null) {
+        Message message = Message.obtain(handler, CaptureActivityHandler.decode_succeeded, rawResult);
+        message.sendToTarget();
+      }
+    } else {
+      if (handler != null) {
+        Message message = Message.obtain(handler, CaptureActivityHandler.decode_failed);
+        message.sendToTarget();
+      }
+    }
+  }
+}

+ 231 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/DecodeHintManager.java

@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2013 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android;
+
+import java.util.EnumMap;
+import java.util.Map;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.google.zxing.DecodeHintType;
+
+/**
+ * @author Lachezar Dobrev
+ */
+final class DecodeHintManager {
+  
+  private static final String TAG = DecodeHintManager.class.getSimpleName();
+
+  // This pattern is used in decoding integer arrays.
+//  private static final Pattern COMMA = Pattern.compile(",");
+
+  private DecodeHintManager() {}
+
+//  /**
+//   * <p>Split a query string into a list of name-value pairs.</p>
+//   *
+//   * <p>This is an alternative to the {@link Uri#getQueryParameterNames()} and
+//   * {@link Uri#getQueryParameters(String)}, which are quirky and not suitable
+//   * for exist-only Uri parameters.</p>
+//   *
+//   * <p>This method ignores multiple parameters with the same name and returns the
+//   * first one only. This is technically incorrect, but should be acceptable due
+//   * to the method of processing Hints: no multiple values for a hint.</p>
+//   *
+//   * @param query query to split
+//   * @return name-value pairs
+//   */
+//  private static Map<String,String> splitQuery(String query) {
+//    Map<String,String> map = new HashMap<>();
+//    int pos = 0;
+//    while (pos < query.length()) {
+//      if (query.charAt(pos) == '&') {
+//        // Skip consecutive ampersand separators.
+//        pos ++;
+//        continue;
+//      }
+//      int amp = query.indexOf('&', pos);
+//      int equ = query.indexOf('=', pos);
+//      if (amp < 0) {
+//        // This is the last element in the query, no more ampersand elements.
+//        String name;
+//        String text;
+//        if (equ < 0) {
+//          // No equal sign
+//          name = query.substring(pos);
+//          name = name.replace('+', ' '); // Preemptively decode +
+//          name = Uri.decode(name);
+//          text = "";
+//        } else {
+//          // Split name and text.
+//          name = query.substring(pos, equ);
+//          name = name.replace('+', ' '); // Preemptively decode +
+//          name = Uri.decode(name);
+//          text = query.substring(equ + 1);
+//          text = text.replace('+', ' '); // Preemptively decode +
+//          text = Uri.decode(text);
+//        }
+//        if (!map.containsKey(name)) {
+//          map.put(name, text);
+//        }
+//        break;
+//      }
+//      if (equ < 0 || equ > amp) {
+//        // No equal sign until the &: this is a simple parameter with no value.
+//        String name = query.substring(pos, amp);
+//        name = name.replace('+', ' '); // Preemptively decode +
+//        name = Uri.decode(name);
+//        if (!map.containsKey(name)) {
+//          map.put(name, "");
+//        }
+//        pos = amp + 1;
+//        continue;
+//      }
+//      String name = query.substring(pos, equ);
+//      name = name.replace('+', ' '); // Preemptively decode +
+//      name = Uri.decode(name);
+//      String text = query.substring(equ + 1, amp);
+//      text = text.replace('+', ' '); // Preemptively decode +
+//      text = Uri.decode(text);
+//      if (!map.containsKey(name)) {
+//        map.put(name, text);
+//      }
+//      pos = amp + 1;
+//    }
+//    return map;
+//  }
+
+//  static Map<DecodeHintType,?> parseDecodeHints(Uri inputUri) {
+//    String query = inputUri.getEncodedQuery();
+//    if (query == null || query.isEmpty()) {
+//      return null;
+//    }
+//
+//    // Extract parameters
+//    Map<String, String> parameters = splitQuery(query);
+//
+//    Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
+//
+//    for (DecodeHintType hintType: DecodeHintType.values()) {
+//
+//      if (hintType == DecodeHintType.CHARACTER_SET ||
+//          hintType == DecodeHintType.NEED_RESULT_POINT_CALLBACK ||
+//          hintType == DecodeHintType.POSSIBLE_FORMATS) {
+//        continue; // This hint is specified in another way
+//      }
+//
+//      String parameterName = hintType.name();
+//      String parameterText = parameters.get(parameterName);
+//      if (parameterText == null) {
+//        continue;
+//      }
+//      if (hintType.getValueType().equals(Object.class)) {
+//        // This is an unspecified type of hint content. Use the value as is.
+//        // TODO: Can we make a different assumption on this?
+//        hints.put(hintType, parameterText);
+//        continue;
+//      }
+//      if (hintType.getValueType().equals(Void.class)) {
+//        // Void hints are just flags: use the constant specified by DecodeHintType
+//        hints.put(hintType, Boolean.TRUE);
+//        continue;
+//      }
+//      if (hintType.getValueType().equals(String.class)) {
+//        // A string hint: use the decoded value.
+//        hints.put(hintType, parameterText);
+//        continue;
+//      }
+//      if (hintType.getValueType().equals(Boolean.class)) {
+//        // A boolean hint: a few values for false, everything else is true.
+//        // An empty parameter is simply a flag-style parameter, assuming true
+//        if (parameterText.isEmpty()) {
+//          hints.put(hintType, Boolean.TRUE);
+//        } else if ("0".equals(parameterText) ||
+//                   "false".equalsIgnoreCase(parameterText) ||
+//                   "no".equalsIgnoreCase(parameterText)) {
+//          hints.put(hintType, Boolean.FALSE);
+//        } else {
+//          hints.put(hintType, Boolean.TRUE);
+//        }
+//
+//        continue;
+//      }
+//      if (hintType.getValueType().equals(int[].class)) {
+//        // An integer array. Used to specify valid lengths.
+//        // Strip a trailing comma as in Java style array initialisers.
+//        if (!parameterText.isEmpty() && parameterText.charAt(parameterText.length() - 1) == ',') {
+//          parameterText = parameterText.substring(0, parameterText.length() - 1);
+//        }
+//        String[] values = COMMA.split(parameterText);
+//        int[] array = new int[values.length];
+//        for (int i = 0; i < values.length; i++) {
+//          try {
+//            array[i] = Integer.parseInt(values[i]);
+//          } catch (NumberFormatException ignored) {
+//            Log.w(TAG, "Skipping array of integers hint " + hintType + " due to invalid numeric value");
+//            array = null;
+//            break;
+//          }
+//        }
+//        if (array != null) {
+//          hints.put(hintType, array);
+//        }
+//        continue;
+//      }
+//      Log.w(TAG, "Unsupported hint type '" + hintType + "' of type " + hintType.getValueType());
+//    }
+//
+//    return hints;
+//  }
+
+  static Map<DecodeHintType, Object> parseDecodeHints(Intent intent) {
+    Bundle extras = intent.getExtras();
+    if (extras == null || extras.isEmpty()) {
+      return null;
+    }
+    Map<DecodeHintType,Object> hints = new EnumMap<>(DecodeHintType.class);
+
+    for (DecodeHintType hintType: DecodeHintType.values()) {
+
+      if (hintType == DecodeHintType.CHARACTER_SET ||
+          hintType == DecodeHintType.NEED_RESULT_POINT_CALLBACK ||
+          hintType == DecodeHintType.POSSIBLE_FORMATS) {
+        continue; // This hint is specified in another way
+      }
+
+      String hintName = hintType.name();
+      if (extras.containsKey(hintName)) {
+        if (hintType.getValueType().equals(Void.class)) {
+          // Void hints are just flags: use the constant specified by the DecodeHintType
+          hints.put(hintType, Boolean.TRUE);
+        } else {
+          Object hintData = extras.get(hintName);
+          if (hintType.getValueType().isInstance(hintData)) {
+            hints.put(hintType, hintData);
+          } else {
+            Log.w(TAG, "Ignoring hint " + hintType + " because it is not a " + hintType.getValueType());
+          }
+        }
+      }
+    }
+
+    return hints;
+  }
+
+}

+ 90 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/DecodeThread.java

@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2008 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android;
+
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.DecodeHintType;
+import com.google.zxing.ResultPointCallback;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * This thread does all the heavy lifting of decoding the images.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+final class DecodeThread extends Thread {
+
+  private final CaptureActivity activity;
+  private final Map<DecodeHintType,Object> hints;
+  private Handler handler;
+  private final CountDownLatch handlerInitLatch;
+
+  DecodeThread(CaptureActivity activity,
+               Collection<BarcodeFormat> decodeFormats,
+               Map<DecodeHintType,?> baseHints,
+               String characterSet,
+               ResultPointCallback resultPointCallback) {
+
+      this.activity = activity;
+      handlerInitLatch = new CountDownLatch(1);
+
+      hints = new EnumMap<>(DecodeHintType.class);
+      if (baseHints != null) {
+          hints.putAll(baseHints);
+      }
+
+      // The prefs can't change while the thread is running, so pick them up once here.
+      if (decodeFormats == null || decodeFormats.isEmpty()) {
+          decodeFormats = EnumSet.noneOf(BarcodeFormat.class);
+          decodeFormats.addAll(DecodeFormatManager.PRODUCT_FORMATS);
+          decodeFormats.addAll(DecodeFormatManager.INDUSTRIAL_FORMATS);
+          decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS);
+      }
+      hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats);
+
+      if (characterSet != null) {
+          hints.put(DecodeHintType.CHARACTER_SET, characterSet);
+      }
+      hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, resultPointCallback);
+  }
+
+  Handler getHandler() {
+    try {
+      handlerInitLatch.await();
+    } catch (InterruptedException ie) {
+      // continue?
+    }
+    return handler;
+  }
+
+  @Override
+  public void run() {
+    Looper.prepare();
+    handler = new DecodeHandler(activity, hints);
+    handlerInitLatch.countDown();
+    Looper.loop();
+  }
+
+}

+ 49 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/FinishListener.java

@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2010 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android;
+
+import android.app.Activity;
+import android.content.DialogInterface;
+
+/**
+ * Simple listener used to exit the app in a few cases.
+ *
+ * @author Sean Owen
+ */
+public final class FinishListener implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+
+  private final Activity activityToFinish;
+
+  public FinishListener(Activity activityToFinish) {
+    this.activityToFinish = activityToFinish;
+  }
+
+  @Override
+  public void onCancel(DialogInterface dialogInterface) {
+    run();
+  }
+
+  @Override
+  public void onClick(DialogInterface dialogInterface, int i) {
+    run();
+  }
+
+  private void run() {
+    activityToFinish.finish();
+  }
+
+}

+ 122 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/InactivityTimer.java

@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2010 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android;
+
+import android.app.Activity;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncTask;
+import android.os.BatteryManager;
+import android.util.Log;
+
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * Finishes an activity after a period of inactivity if the device is on battery power.
+ */
+final class InactivityTimer {
+
+  private static final String TAG = InactivityTimer.class.getSimpleName();
+
+  private static final long INACTIVITY_DELAY_MS = 5 * 60 * 1000L;
+
+  private final Activity activity;
+  private final BroadcastReceiver powerStatusReceiver;
+  private boolean registered;
+  private AsyncTask<Object,Object,Object> inactivityTask;
+
+  InactivityTimer(Activity activity) {
+    this.activity = activity;
+    powerStatusReceiver = new PowerStatusReceiver();
+    registered = false;
+    onActivity();
+  }
+
+  synchronized void onActivity() {
+    cancel();
+    inactivityTask = new InactivityAsyncTask();
+    try {
+      inactivityTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    } catch (RejectedExecutionException ree) {
+      Log.w(TAG, "Couldn't schedule inactivity task; ignoring"); 
+    }
+  }
+
+  synchronized void onPause() {
+    cancel();
+    if (registered) {
+      activity.unregisterReceiver(powerStatusReceiver);
+      registered = false;
+    } else {
+      Log.w(TAG, "PowerStatusReceiver was never registered?");
+    }
+  }
+
+  synchronized void onResume() {
+    if (registered) {
+      Log.w(TAG, "PowerStatusReceiver was already registered?");
+    } else {
+      activity.registerReceiver(powerStatusReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
+      registered = true;
+    }
+    onActivity();
+  }
+
+  private synchronized void cancel() {
+    AsyncTask<?,?,?> task = inactivityTask;
+    if (task != null) {
+      task.cancel(true);
+      inactivityTask = null;
+    }
+  }
+
+  void shutdown() {
+    cancel();
+  }
+
+  private final class PowerStatusReceiver extends BroadcastReceiver {
+    @Override
+    public void onReceive(Context context, Intent intent) {
+      if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
+        // 0 indicates that we're on battery
+        boolean onBatteryNow = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1) <= 0;
+        if (onBatteryNow) {
+          InactivityTimer.this.onActivity();
+        } else {
+          InactivityTimer.this.cancel();
+        }
+      }
+    }
+  }
+
+  private final class InactivityAsyncTask extends AsyncTask<Object,Object,Object> {
+    @Override
+    protected Object doInBackground(Object... objects) {
+      try {
+        Thread.sleep(INACTIVITY_DELAY_MS);
+        Log.i(TAG, "Finishing activity due to inactivity");
+        activity.finish();
+      } catch (InterruptedException e) {
+        // continue without killing
+      }
+      return null;
+    }
+  }
+
+}

+ 201 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/Intents.java

@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2008 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android;
+
+import android.content.Intent;
+
+/**
+ * This class provides the constants to use when sending an Intent to Barcode Scanner.
+ * These strings are effectively API and cannot be changed.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class Intents {
+
+  /**
+   * Constants related to the {@link Scan#ACTION} Intent.
+   */
+  public static final class Scan {
+    /**
+     * Send this intent to open the Barcodes app in scanning mode, find a barcode, and return
+     * the results.
+     */
+    public static final String ACTION = "com.google.zxing.client.android.SCAN";
+
+    /**
+     * Title of the scanner
+     */
+    public static final String SCAN_TITLE = "SCAN_TITLE";
+
+    /**
+     * Play beep
+     */
+    public static final String KEY_PLAY_BEEP = "KEY_PLAY_BEEP";
+
+    /**
+     * By default, sending this will decode all barcodes that we understand. However it
+     * may be useful to limit scanning to certain formats. Use
+     * {@link android.content.Intent#putExtra(String, String)} with one of the values below.
+     *
+     * Setting this is effectively shorthand for setting explicit formats with {@link #FORMATS}.
+     * It is overridden by that setting.
+     */
+    public static final String MODE = "SCAN_MODE";
+
+    /**
+     * Decode only UPC and EAN barcodes. This is the right choice for shopping apps which get
+     * prices, reviews, etc. for products.
+     */
+    public static final String PRODUCT_MODE = "PRODUCT_MODE";
+
+    /**
+     * Decode only 1D barcodes.
+     */
+    public static final String ONE_D_MODE = "ONE_D_MODE";
+
+    /**
+     * Decode only QR codes.
+     */
+    public static final String QR_CODE_MODE = "QR_CODE_MODE";
+
+    /**
+     * Decode only Data Matrix codes.
+     */
+    public static final String DATA_MATRIX_MODE = "DATA_MATRIX_MODE";
+
+    /**
+     * Decode only Aztec.
+     */
+    public static final String AZTEC_MODE = "AZTEC_MODE";
+
+    /**
+     * Decode only PDF417.
+     */
+    public static final String PDF417_MODE = "PDF417_MODE";
+
+    /**
+     * Comma-separated list of formats to scan for. The values must match the names of
+     * {@link com.google.zxing.BarcodeFormat}s, e.g. {@link com.google.zxing.BarcodeFormat#EAN_13}.
+     * Example: "EAN_13,EAN_8,QR_CODE". This overrides {@link #MODE}.
+     */
+    public static final String FORMATS = "SCAN_FORMATS";
+
+    /**
+     * Optional parameter to specify the id of the camera from which to recognize barcodes.
+     * Overrides the default camera that would otherwise would have been selected.
+     * If provided, should be an int.
+     */
+    public static final String CAMERA_ID = "SCAN_CAMERA_ID";
+
+    /**
+     * @see com.google.zxing.DecodeHintType#CHARACTER_SET
+     */
+    public static final String CHARACTER_SET = "CHARACTER_SET";
+
+    /**
+     * Optional parameters to specify the width and height of the scanning rectangle in pixels.
+     * The app will try to honor these, but will clamp them to the size of the preview frame.
+     * You should specify both or neither, and pass the size as an int.
+     */
+    public static final String WIDTH = "SCAN_WIDTH";
+    public static final String HEIGHT = "SCAN_HEIGHT";
+
+    /**
+     * Desired duration in milliseconds for which to pause after a successful scan before
+     * returning to the calling intent. Specified as a long, not an integer!
+     * For example: 1000L, not 1000.
+     */
+    public static final String RESULT_DISPLAY_DURATION_MS = "RESULT_DISPLAY_DURATION_MS";
+
+    /**
+     * Prompt to show on-screen when scanning by intent. Specified as a {@link String}.
+     */
+    public static final String PROMPT_MESSAGE = "PROMPT_MESSAGE";
+
+    /**
+     * Setting the error message when the camera initial failed
+     */
+    public static final String ERROR_MESSAGE = "ERROR_MESSAGE";
+
+    /**
+     * Setting the error message when permission denied
+     */
+    public static final String PERMISSION_DENIED_MESSAGE = "PERMISSION_DENIED_MESSAGE";
+
+    /**
+     * Setting the message confirm text when the camera initial failed or permission denied
+     */
+    public static final String MESSAGE_CONFIRM_TEXT = "MESSAGE_CONFIRM_TEXT";
+
+    /**
+     * Setting the message cancel text when permission denied
+     */
+    public static final String MESSAGE_CANCEL_TEXT = "MESSAGE_CANCEL_TEXT";
+
+    /**
+     * laser color
+     * default #ffff55ff
+     */
+    public static final String LASER_COLOR = "LASER_COLOR";
+  }
+
+  /**
+   * Constants related to the {@link Encode#ACTION} Intent.
+   */
+  public static final class Encode {
+    /**
+     * Send this intent to encode a piece of data as a QR code and display it full screen, so
+     * that another person can scan the barcode from your screen.
+     */
+    public static final String ACTION = "com.google.zxing.client.android.ENCODE";
+
+    /**
+     * The data to encode. Use {@link android.content.Intent#putExtra(String, String)} or
+     * {@link android.content.Intent#putExtra(String, android.os.Bundle)}, 
+     * depending on the type and format specified. Non-QR Code formats should
+     * just use a String here. For QR Code, see Contents for details.
+     */
+    public static final String DATA = "ENCODE_DATA";
+
+    /**
+     * The type of data being supplied if the format is QR Code. Use
+     * {@link android.content.Intent#putExtra(String, String)} with one of {@link Contents.Type}.
+     */
+    public static final String TYPE = "ENCODE_TYPE";
+
+    /**
+     * The barcode format to be displayed. If this isn't specified or is blank,
+     * it defaults to QR Code. Use {@link android.content.Intent#putExtra(String, String)}, where
+     * format is one of {@link com.google.zxing.BarcodeFormat}.
+     */
+    public static final String FORMAT = "ENCODE_FORMAT";
+
+    /**
+     * Normally the contents of the barcode are displayed to the user in a TextView. Setting this
+     * boolean to false will hide that TextView, showing only the encode barcode.
+     */
+    public static final String SHOW_CONTENTS = "ENCODE_SHOW_CONTENTS";
+
+    private Encode() {
+    }
+  }
+
+  // Not the best place for this, but, better than a new class
+  // Should be FLAG_ACTIVITY_NEW_DOCUMENT in API 21+.
+  // Defined once here because the current value is deprecated, so generates just one warning
+  public static final int FLAG_NEW_DOC = Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET;
+}

+ 156 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/ViewfinderView.java

@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2008 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android;
+
+import com.google.zxing.client.android.camera.CameraManager;
+import com.alva.flutter.plugin.scanner.R;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+import android.util.Log;
+
+/**
+ * This view is overlaid on top of the camera preview. It adds the viewfinder rectangle and partial
+ * transparency outside it, as well as the laser scanner animation and result points.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+public final class ViewfinderView extends View {
+
+  private static final long ANIMATION_DELAY = 50L;
+
+  private static final int SPEED_DISTANCE = 10;
+  private static final int POINT_SIZE = 6;
+
+  private CameraManager cameraManager;
+  private final Paint paint;
+  private final Path path;
+  private final int maskColor;
+  private static int laserColor;
+
+  private int slideTop;
+  private int slideMid;
+  private Bitmap netBitmap;
+
+  // This constructor is used when the class is built from an XML resource.
+  public ViewfinderView(Context context, AttributeSet attrs) {
+    super(context, attrs);
+
+    // Initialize these once for performance rather than calling them every time in onDraw().
+    path = new Path();
+    paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    Resources resources = getResources();
+    maskColor = resources.getColor(R.color.viewfinder_mask);
+    laserColor = resources.getColor(R.color.viewfinder_laser);
+  }
+
+  public void setCameraManager(CameraManager cameraManager, int laserColor) {
+    this.cameraManager = cameraManager;
+    if(laserColor != 0)
+      ViewfinderView.laserColor = laserColor;
+  }
+
+  @SuppressLint("DrawAllocation")
+  @Override
+  public void onDraw(Canvas canvas) {
+    if (cameraManager == null) {
+      return; // not ready yet, early draw before done configuring
+    }
+    Rect frame = cameraManager.getFramingRect();
+    Rect previewFrame = cameraManager.getFramingRectInPreview();
+    if (frame == null || previewFrame == null) {
+      return;
+    }
+    int width = getWidth();
+    int height = getHeight();
+
+    // Draw the exterior (i.e. outside the framing rect) darkened
+    paint.reset();
+    paint.setColor(maskColor);
+    canvas.drawRect(0, 0, width, frame.top, paint);
+    canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint);
+    canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, paint);
+    canvas.drawRect(0, frame.bottom + 1, width, height, paint);
+
+    // Draw a red "laser scanner" line through the middle to show decoding is active
+    if (path.isEmpty()) {
+      int iWidth = width / 15;
+      path.moveTo(frame.left + iWidth, frame.top);
+      path.lineTo(frame.left, frame.top);
+      path.lineTo(frame.left, frame.top + iWidth);
+      path.moveTo(frame.right - iWidth, frame.top);
+      path.lineTo(frame.right, frame.top);
+      path.lineTo(frame.right, frame.top + iWidth);
+      path.moveTo(frame.left + iWidth, frame.bottom);
+      path.lineTo(frame.left, frame.bottom);
+      path.lineTo(frame.left, frame.bottom - iWidth);
+      path.moveTo(frame.right - iWidth, frame.bottom);
+      path.lineTo(frame.right, frame.bottom);
+      path.lineTo(frame.right, frame.bottom - iWidth);
+    }
+    paint.setColor(laserColor);
+    paint.setStyle(Paint.Style.STROKE);
+    paint.setStrokeWidth(POINT_SIZE);
+    canvas.drawPath(path, paint);
+    canvas.saveLayerAlpha(0, 0, width, height, 120, Canvas.ALL_SAVE_FLAG);
+
+    if (slideTop <= SPEED_DISTANCE || slideTop >= frame.bottom) {
+      slideTop = frame.top;
+    }
+    if (slideMid == 0)
+      slideMid = frame.top + (frame.bottom - frame.top) / 2;
+    if (slideTop < slideMid - 100) {
+      paint.setAlpha((int) ((slideTop - frame.top) / (slideMid - frame.top - 100.0) * 255));
+    } else if (slideTop > slideMid + 100) {
+      paint.setAlpha((int) ((slideMid + 100.0 - frame.top) / (slideTop - frame.top) * 255));
+    } else
+      paint.setAlpha(255);
+    if (netBitmap == null)
+      netBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.scan_finder_net);
+
+    Rect lineRect = new Rect();
+    lineRect.left = frame.left;
+    lineRect.right = frame.right;
+    lineRect.top = slideTop - 400;
+    lineRect.bottom = slideTop;
+    canvas.drawBitmap(netBitmap, null, lineRect, paint);
+    canvas.clipRect(0, 0, width, frame.top);
+    canvas.drawColor(maskColor, PorterDuff.Mode.CLEAR);
+
+    slideTop += SPEED_DISTANCE;
+
+    // Request another update at the animation interval, but only repaint the laser line,
+    // not the entire viewfinder mask.
+    postInvalidateDelayed(ANIMATION_DELAY, frame.left - POINT_SIZE, frame.top,
+        frame.right + POINT_SIZE, frame.bottom);
+  }
+
+  public void drawViewfinder() {
+    invalidate();
+  }
+
+}

+ 126 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/AutoFocusManager.java

@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2012 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android.camera;
+
+import android.annotation.SuppressLint;
+import android.hardware.Camera;
+import android.os.AsyncTask;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.RejectedExecutionException;
+
+final class AutoFocusManager implements Camera.AutoFocusCallback {
+
+  private static final String TAG = AutoFocusManager.class.getSimpleName();
+
+  private static final long AUTO_FOCUS_INTERVAL_MS = 2000L;
+  private static final Collection<String> FOCUS_MODES_CALLING_AF;
+  static {
+    FOCUS_MODES_CALLING_AF = new ArrayList<>(2);
+    FOCUS_MODES_CALLING_AF.add(Camera.Parameters.FOCUS_MODE_AUTO);
+    FOCUS_MODES_CALLING_AF.add(Camera.Parameters.FOCUS_MODE_MACRO);
+  }
+
+  private boolean stopped;
+  private boolean focusing;
+  private final boolean useAutoFocus;
+  private final Camera camera;
+  private AsyncTask<?,?,?> outstandingTask;
+
+  AutoFocusManager(Camera camera) {
+    this.camera = camera;
+    String currentFocusMode = camera.getParameters().getFocusMode();
+    useAutoFocus = FOCUS_MODES_CALLING_AF.contains(currentFocusMode);
+    Log.i(TAG, "Current focus mode '" + currentFocusMode + "'; use auto focus? " + useAutoFocus);
+    start();
+  }
+
+  @Override
+  public synchronized void onAutoFocus(boolean success, Camera theCamera) {
+    focusing = false;
+    autoFocusAgainLater();
+  }
+
+  private synchronized void autoFocusAgainLater() {
+    if (!stopped && outstandingTask == null) {
+      AutoFocusTask newTask = new AutoFocusTask();
+      try {
+        newTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+        outstandingTask = newTask;
+      } catch (RejectedExecutionException ree) {
+        Log.w(TAG, "Could not request auto focus", ree);
+      }
+    }
+  }
+
+  synchronized void start() {
+    if (useAutoFocus) {
+      outstandingTask = null;
+      if (!stopped && !focusing) {
+        try {
+          camera.autoFocus(this);
+          focusing = true;
+        } catch (RuntimeException re) {
+          // Have heard RuntimeException reported in Android 4.0.x+; continue?
+          Log.w(TAG, "Unexpected exception while focusing", re);
+          // Try again later to keep cycle going
+          autoFocusAgainLater();
+        }
+      }
+    }
+  }
+
+  private synchronized void cancelOutstandingTask() {
+    if (outstandingTask != null) {
+      if (outstandingTask.getStatus() != AsyncTask.Status.FINISHED) {
+        outstandingTask.cancel(true);
+      }
+      outstandingTask = null;
+    }
+  }
+
+  synchronized void stop() {
+    stopped = true;
+    if (useAutoFocus) {
+      cancelOutstandingTask();
+      // Doesn't hurt to call this even if not focusing
+      try {
+        camera.cancelAutoFocus();
+      } catch (RuntimeException re) {
+        // Have heard RuntimeException reported in Android 4.0.x+; continue?
+        Log.w(TAG, "Unexpected exception while cancelling focusing", re);
+      }
+    }
+  }
+
+  @SuppressLint("StaticFieldLeak")
+  private final class AutoFocusTask extends AsyncTask<Object,Object,Object> {
+    @Override
+    protected Object doInBackground(Object... voids) {
+      try {
+        Thread.sleep(AUTO_FOCUS_INTERVAL_MS);
+      } catch (InterruptedException e) {
+        // continue
+      }
+      start();
+      return null;
+    }
+  }
+
+}

+ 237 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/CameraConfigurationManager.java

@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2010 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android.camera;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Point;
+import android.hardware.Camera;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.view.Display;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import com.google.zxing.client.android.camera.open.CameraFacing;
+import com.google.zxing.client.android.camera.open.OpenCamera;
+
+/**
+ * A class which deals with reading, parsing, and setting the camera parameters which are used to
+ * configure the camera hardware.
+ */
+@SuppressWarnings("deprecation") // camera APIs
+final class CameraConfigurationManager {
+
+  private static final String TAG = "CameraConfiguration";
+
+  private final Context context;
+  private int cwNeededRotation;
+  private int cwRotationFromDisplayToCamera;
+  private Point screenResolution;
+  private Point cameraResolution;
+  private Point bestPreviewSize;
+  private Point previewSizeOnScreen;
+
+  CameraConfigurationManager(Context context) {
+    this.context = context;
+  }
+
+  /**
+   * Reads, one time, values from the camera that are needed by the app.
+   */
+  void initFromCameraParameters(OpenCamera camera) {
+    Camera.Parameters parameters = camera.getCamera().getParameters();
+    WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+    Display display = manager.getDefaultDisplay();
+
+    int displayRotation = display.getRotation();
+    int cwRotationFromNaturalToDisplay;
+    switch (displayRotation) {
+      case Surface.ROTATION_0:
+        cwRotationFromNaturalToDisplay = 0;
+        break;
+      case Surface.ROTATION_90:
+        cwRotationFromNaturalToDisplay = 90;
+        break;
+      case Surface.ROTATION_180:
+        cwRotationFromNaturalToDisplay = 180;
+        break;
+      case Surface.ROTATION_270:
+        cwRotationFromNaturalToDisplay = 270;
+        break;
+      default:
+        // Have seen this return incorrect values like -90
+        if (displayRotation % 90 == 0) {
+          cwRotationFromNaturalToDisplay = (360 + displayRotation) % 360;
+        } else {
+          throw new IllegalArgumentException("Bad rotation: " + displayRotation);
+        }
+    }
+    Log.i(TAG, "Display at: " + cwRotationFromNaturalToDisplay);
+
+    int cwRotationFromNaturalToCamera = camera.getOrientation();
+    Log.i(TAG, "Camera at: " + cwRotationFromNaturalToCamera);
+
+    // Still not 100% sure about this. But acts like we need to flip this:
+    if (camera.getFacing() == CameraFacing.FRONT) {
+      cwRotationFromNaturalToCamera = (360 - cwRotationFromNaturalToCamera) % 360;
+      Log.i(TAG, "Front camera overriden to: " + cwRotationFromNaturalToCamera);
+    }
+
+    /*
+    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+    String overrideRotationString;
+    if (camera.getFacing() == CameraFacing.FRONT) {
+      overrideRotationString = prefs.getString(PreferencesActivity.KEY_FORCE_CAMERA_ORIENTATION_FRONT, null);
+    } else {
+      overrideRotationString = prefs.getString(PreferencesActivity.KEY_FORCE_CAMERA_ORIENTATION, null);
+    }
+    if (overrideRotationString != null && !"-".equals(overrideRotationString)) {
+      Log.i(TAG, "Overriding camera manually to " + overrideRotationString);
+      cwRotationFromNaturalToCamera = Integer.parseInt(overrideRotationString);
+    }
+     */
+
+    cwRotationFromDisplayToCamera =
+        (360 + cwRotationFromNaturalToCamera - cwRotationFromNaturalToDisplay) % 360;
+    Log.i(TAG, "Final display orientation: " + cwRotationFromDisplayToCamera);
+    if (camera.getFacing() == CameraFacing.FRONT) {
+      Log.i(TAG, "Compensating rotation for front camera");
+      cwNeededRotation = (360 - cwRotationFromDisplayToCamera) % 360;
+    } else {
+      cwNeededRotation = cwRotationFromDisplayToCamera;
+    }
+    Log.i(TAG, "Clockwise rotation from display to camera: " + cwNeededRotation);
+
+    Point theScreenResolution = new Point();
+    display.getSize(theScreenResolution);
+    screenResolution = theScreenResolution;
+    Log.i(TAG, "Screen resolution in current orientation: " + screenResolution);
+    cameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
+    Log.i(TAG, "Camera resolution: " + cameraResolution);
+    bestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
+    Log.i(TAG, "Best available preview size: " + bestPreviewSize);
+
+    boolean isScreenPortrait = screenResolution.x < screenResolution.y;
+    boolean isPreviewSizePortrait = bestPreviewSize.x < bestPreviewSize.y;
+
+    if (isScreenPortrait == isPreviewSizePortrait) {
+      previewSizeOnScreen = bestPreviewSize;
+    } else {
+      previewSizeOnScreen = new Point(bestPreviewSize.y, bestPreviewSize.x);
+    }
+    Log.i(TAG, "Preview size on screen: " + previewSizeOnScreen);
+  }
+
+  void setDesiredCameraParameters(OpenCamera camera, boolean safeMode) {
+
+    Camera theCamera = camera.getCamera();
+    Camera.Parameters parameters = theCamera.getParameters();
+
+    if (parameters == null) {
+      Log.w(TAG, "Device error: no camera parameters are available. Proceeding without configuration.");
+      return;
+    }
+
+    Log.i(TAG, "Initial camera parameters: " + parameters.flatten());
+
+    if (safeMode) {
+      Log.w(TAG, "In camera config safe mode -- most settings will not be honored");
+    }
+
+    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+    initializeTorch(parameters, prefs);
+
+    CameraConfigurationUtils.setFocus(parameters, true, true, safeMode);
+
+    if (!safeMode) {
+      CameraConfigurationUtils.setBarcodeSceneMode(parameters);
+      CameraConfigurationUtils.setVideoStabilization(parameters);
+      CameraConfigurationUtils.setFocusArea(parameters);
+      CameraConfigurationUtils.setMetering(parameters);
+
+      //SetRecordingHint to true also a workaround for low framerate on Nexus 4
+      //https://stackoverflow.com/questions/14131900/extreme-camera-lag-on-nexus-4
+      parameters.setRecordingHint(true);
+
+    }
+
+    parameters.setPreviewSize(bestPreviewSize.x, bestPreviewSize.y);
+
+    theCamera.setParameters(parameters);
+
+    theCamera.setDisplayOrientation(cwRotationFromDisplayToCamera);
+
+    Camera.Parameters afterParameters = theCamera.getParameters();
+    Camera.Size afterSize = afterParameters.getPreviewSize();
+    if (afterSize != null && (bestPreviewSize.x != afterSize.width || bestPreviewSize.y != afterSize.height)) {
+      Log.w(TAG, "Camera said it supported preview size " + bestPreviewSize.x + 'x' + bestPreviewSize.y +
+          ", but after setting it, preview size is " + afterSize.width + 'x' + afterSize.height);
+      bestPreviewSize.x = afterSize.width;
+      bestPreviewSize.y = afterSize.height;
+    }
+  }
+
+  Point getBestPreviewSize() {
+    return bestPreviewSize;
+  }
+
+  Point getPreviewSizeOnScreen() {
+    return previewSizeOnScreen;
+  }
+
+  Point getCameraResolution() {
+    return cameraResolution;
+  }
+
+  Point getScreenResolution() {
+    return screenResolution;
+  }
+
+  int getCWNeededRotation() {
+    return cwNeededRotation;
+  }
+
+  boolean getTorchState(Camera camera) {
+    if (camera != null) {
+      Camera.Parameters parameters = camera.getParameters();
+      if (parameters != null) {
+        String flashMode = parameters.getFlashMode();
+        return
+            Camera.Parameters.FLASH_MODE_ON.equals(flashMode) ||
+            Camera.Parameters.FLASH_MODE_TORCH.equals(flashMode);
+      }
+    }
+    return false;
+  }
+
+  void setTorch(Camera camera, boolean newSetting) {
+    Camera.Parameters parameters = camera.getParameters();
+    doSetTorch(parameters, newSetting);
+    camera.setParameters(parameters);
+  }
+
+  private void initializeTorch(Camera.Parameters parameters, SharedPreferences prefs) {
+    doSetTorch(parameters, false);
+  }
+
+  private void doSetTorch(Camera.Parameters parameters, boolean newSetting) {
+    CameraConfigurationUtils.setTorch(parameters, newSetting);
+  }
+
+}

+ 433 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/CameraConfigurationUtils.java

@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2014 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android.camera;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.Camera;
+import android.os.Build;
+import android.util.Log;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Utility methods for configuring the Android camera.
+ *
+ * @author Sean Owen
+ */
+// camera APIs
+public final class CameraConfigurationUtils {
+
+  private static final String TAG = "CameraConfiguration";
+
+  private static final Pattern SEMICOLON = Pattern.compile(";");
+
+  private static final int MIN_PREVIEW_PIXELS = 480 * 320; // normal screen
+  private static final float MAX_EXPOSURE_COMPENSATION = 1.5f;
+  private static final float MIN_EXPOSURE_COMPENSATION = 0.0f;
+  private static final double MAX_ASPECT_DISTORTION = 0.15;
+  private static final int MIN_FPS = 10;
+  private static final int MAX_FPS = 20;
+  private static final int AREA_PER_1000 = 400;
+
+  private CameraConfigurationUtils() {
+  }
+
+  static void setFocus(Camera.Parameters parameters,
+                       boolean autoFocus,
+                       boolean disableContinuous,
+                       boolean safeMode) {
+    List<String> supportedFocusModes = parameters.getSupportedFocusModes();
+    String focusMode = null;
+    if (autoFocus) {
+      if (safeMode || disableContinuous) {
+        focusMode = findSettableValue("focus mode",
+                                       supportedFocusModes,
+                                       Camera.Parameters.FOCUS_MODE_AUTO);
+      } else {
+        focusMode = findSettableValue("focus mode",
+                                      supportedFocusModes,
+                                      Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE,
+                                      Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO,
+                                      Camera.Parameters.FOCUS_MODE_AUTO);
+      }
+    }
+    // Maybe selected auto-focus but not available, so fall through here:
+    if (!safeMode && focusMode == null) {
+      focusMode = findSettableValue("focus mode",
+                                    supportedFocusModes,
+                                    Camera.Parameters.FOCUS_MODE_MACRO,
+                                    Camera.Parameters.FOCUS_MODE_EDOF);
+    }
+    if (focusMode != null) {
+      if (focusMode.equals(parameters.getFocusMode())) {
+        Log.i(TAG, "Focus mode already set to " + focusMode);
+      } else {
+        parameters.setFocusMode(focusMode);
+      }
+    }
+  }
+
+  public static void setTorch(Camera.Parameters parameters, boolean on) {
+    List<String> supportedFlashModes = parameters.getSupportedFlashModes();
+    String flashMode;
+    if (on) {
+      flashMode = findSettableValue("flash mode",
+                                    supportedFlashModes,
+                                    Camera.Parameters.FLASH_MODE_TORCH,
+                                    Camera.Parameters.FLASH_MODE_ON);
+    } else {
+      flashMode = findSettableValue("flash mode",
+                                    supportedFlashModes,
+                                    Camera.Parameters.FLASH_MODE_OFF);
+    }
+    if (flashMode != null) {
+      if (flashMode.equals(parameters.getFlashMode())) {
+        Log.i(TAG, "Flash mode already set to " + flashMode);
+      } else {
+        Log.i(TAG, "Setting flash mode to " + flashMode);
+        parameters.setFlashMode(flashMode);
+      }
+    }
+  }
+
+  public static void setBestExposure(Camera.Parameters parameters, boolean lightOn) {
+    int minExposure = parameters.getMinExposureCompensation();
+    int maxExposure = parameters.getMaxExposureCompensation();
+    float step = parameters.getExposureCompensationStep();
+    if ((minExposure != 0 || maxExposure != 0) && step > 0.0f) {
+      // Set low when light is on
+      float targetCompensation = lightOn ? MIN_EXPOSURE_COMPENSATION : MAX_EXPOSURE_COMPENSATION;
+      int compensationSteps = Math.round(targetCompensation / step);
+      float actualCompensation = step * compensationSteps;
+      // Clamp value:
+      compensationSteps = Math.max(Math.min(compensationSteps, maxExposure), minExposure);
+      if (parameters.getExposureCompensation() == compensationSteps) {
+        Log.i(TAG, "Exposure compensation already set to " + compensationSteps + " / " + actualCompensation);
+      } else {
+        Log.i(TAG, "Setting exposure compensation to " + compensationSteps + " / " + actualCompensation);
+        parameters.setExposureCompensation(compensationSteps);
+      }
+    } else {
+      Log.i(TAG, "Camera does not support exposure compensation");
+    }
+  }
+
+  public static void setBestPreviewFPS(Camera.Parameters parameters) {
+    setBestPreviewFPS(parameters, MIN_FPS, MAX_FPS);
+  }
+
+  public static void setBestPreviewFPS(Camera.Parameters parameters, int minFPS, int maxFPS) {
+    List<int[]> supportedPreviewFpsRanges = parameters.getSupportedPreviewFpsRange();
+    Log.i(TAG, "Supported FPS ranges: " + toString(supportedPreviewFpsRanges));
+    if (supportedPreviewFpsRanges != null && !supportedPreviewFpsRanges.isEmpty()) {
+      int[] suitableFPSRange = null;
+      for (int[] fpsRange : supportedPreviewFpsRanges) {
+        int thisMin = fpsRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX];
+        int thisMax = fpsRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX];
+        if (thisMin >= minFPS * 1000 && thisMax <= maxFPS * 1000) {
+          suitableFPSRange = fpsRange;
+          break;
+        }
+      }
+      if (suitableFPSRange == null) {
+        Log.i(TAG, "No suitable FPS range?");
+      } else {
+        int[] currentFpsRange = new int[2];
+        parameters.getPreviewFpsRange(currentFpsRange);
+        if (Arrays.equals(currentFpsRange, suitableFPSRange)) {
+          Log.i(TAG, "FPS range already set to " + Arrays.toString(suitableFPSRange));
+        } else {
+          Log.i(TAG, "Setting FPS range to " + Arrays.toString(suitableFPSRange));
+          parameters.setPreviewFpsRange(suitableFPSRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX],
+                                        suitableFPSRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]);
+        }
+      }
+    }
+  }
+
+  public static void setFocusArea(Camera.Parameters parameters) {
+    if (parameters.getMaxNumFocusAreas() > 0) {
+      Log.i(TAG, "Old focus areas: " + toString(parameters.getFocusAreas()));
+      List<Camera.Area> middleArea = buildMiddleArea(AREA_PER_1000);
+      Log.i(TAG, "Setting focus area to : " + toString(middleArea));
+      parameters.setFocusAreas(middleArea);
+    } else {
+      Log.i(TAG, "Device does not support focus areas");
+    }
+  }
+
+  public static void setMetering(Camera.Parameters parameters) {
+    if (parameters.getMaxNumMeteringAreas() > 0) {
+      Log.i(TAG, "Old metering areas: " + parameters.getMeteringAreas());
+      List<Camera.Area> middleArea = buildMiddleArea(AREA_PER_1000);
+      Log.i(TAG, "Setting metering area to : " + toString(middleArea));
+      parameters.setMeteringAreas(middleArea);
+    } else {
+      Log.i(TAG, "Device does not support metering areas");
+    }
+  }
+
+  private static List<Camera.Area> buildMiddleArea(int areaPer1000) {
+    return Collections.singletonList(
+        new Camera.Area(new Rect(-areaPer1000, -areaPer1000, areaPer1000, areaPer1000), 1));
+  }
+
+  public static void setVideoStabilization(Camera.Parameters parameters) {
+    if (parameters.isVideoStabilizationSupported()) {
+      if (parameters.getVideoStabilization()) {
+        Log.i(TAG, "Video stabilization already enabled");
+      } else {
+        Log.i(TAG, "Enabling video stabilization...");
+        parameters.setVideoStabilization(true);
+      }
+    } else {
+      Log.i(TAG, "This device does not support video stabilization");
+    }
+  }
+
+  public static void setBarcodeSceneMode(Camera.Parameters parameters) {
+    if (Camera.Parameters.SCENE_MODE_BARCODE.equals(parameters.getSceneMode())) {
+      Log.i(TAG, "Barcode scene mode already set");
+      return;
+    }
+    String sceneMode = findSettableValue("scene mode",
+                                         parameters.getSupportedSceneModes(),
+                                         Camera.Parameters.SCENE_MODE_BARCODE);
+    if (sceneMode != null) {
+      parameters.setSceneMode(sceneMode);
+    }
+  }
+
+  public static void setZoom(Camera.Parameters parameters, double targetZoomRatio) {
+    if (parameters.isZoomSupported()) {
+      Integer zoom = indexOfClosestZoom(parameters, targetZoomRatio);
+      if (zoom == null) {
+        return;
+      }
+      if (parameters.getZoom() == zoom) {
+        Log.i(TAG, "Zoom is already set to " + zoom);
+      } else {
+        Log.i(TAG, "Setting zoom to " + zoom);
+        parameters.setZoom(zoom);
+      }
+    } else {
+      Log.i(TAG, "Zoom is not supported");
+    }
+  }
+
+  private static Integer indexOfClosestZoom(Camera.Parameters parameters, double targetZoomRatio) {
+    List<Integer> ratios = parameters.getZoomRatios();
+    Log.i(TAG, "Zoom ratios: " + ratios);
+    int maxZoom = parameters.getMaxZoom();
+    if (ratios == null || ratios.isEmpty() || ratios.size() != maxZoom + 1) {
+      Log.w(TAG, "Invalid zoom ratios!");
+      return null;
+    }
+    double target100 = 100.0 * targetZoomRatio;
+    double smallestDiff = Double.POSITIVE_INFINITY;
+    int closestIndex = 0;
+    for (int i = 0; i < ratios.size(); i++) {
+      double diff = Math.abs(ratios.get(i) - target100);
+      if (diff < smallestDiff) {
+        smallestDiff = diff;
+        closestIndex = i;
+      }
+    }
+    Log.i(TAG, "Chose zoom ratio of " + (ratios.get(closestIndex) / 100.0));
+    return closestIndex;
+  }
+
+  public static void setInvertColor(Camera.Parameters parameters) {
+    if (Camera.Parameters.EFFECT_NEGATIVE.equals(parameters.getColorEffect())) {
+      Log.i(TAG, "Negative effect already set");
+      return;
+    }
+    String colorMode = findSettableValue("color effect",
+                                         parameters.getSupportedColorEffects(),
+                                         Camera.Parameters.EFFECT_NEGATIVE);
+    if (colorMode != null) {
+      parameters.setColorEffect(colorMode);
+    }
+  }
+
+  //edit by alva
+  public static Point findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution) {
+
+    List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes();
+    if (rawSupportedSizes == null) {
+      Log.w(TAG, "Device returned no supported preview sizes; using default");
+      Camera.Size defaultSize = parameters.getPreviewSize();
+      if (defaultSize == null) {
+        throw new IllegalStateException("Parameters contained no preview size!");
+      }
+      return new Point(defaultSize.width, defaultSize.height);
+    }
+
+    if (Log.isLoggable(TAG, Log.INFO)) {
+      StringBuilder previewSizesString = new StringBuilder();
+      for (Camera.Size size : rawSupportedSizes) {
+        previewSizesString.append(size.width).append('x').append(size.height).append(' ');
+      }
+      Log.i(TAG, "Supported preview sizes: " + previewSizesString);
+    }
+
+    double screenAspectRatio = screenResolution.x / (double) screenResolution.y;
+
+    // Find a suitable size, with max resolution
+    int maxResolution = 0;
+    Camera.Size maxResPreviewSize = null;
+    for (Camera.Size size : rawSupportedSizes) {
+      int realWidth = size.width;
+      int realHeight = size.height;
+      int resolution = realWidth * realHeight;
+      if (resolution < MIN_PREVIEW_PIXELS) {
+        continue;
+      }
+
+      //edit by alva, original code is "realWidth < realHeight"
+      boolean isCandidatePortrait = screenResolution.x < screenResolution.y;
+      int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
+      int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight;
+      Log.i(TAG, "Found real screen size: " + realWidth + "x" + realHeight);
+      double aspectRatio = maybeFlippedWidth / (double) maybeFlippedHeight;
+      double distortion = Math.abs(aspectRatio - screenAspectRatio);
+      if (distortion > MAX_ASPECT_DISTORTION) {
+        continue;
+      }
+
+      if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) {
+        Point exactPoint = new Point(realWidth, realHeight);
+        Log.i(TAG, "Found preview size exactly matching screen size: " + exactPoint);
+        return exactPoint;
+      }
+
+      // Resolution is suitable; record the one with max resolution
+      if (resolution > maxResolution) {
+        maxResolution = resolution;
+        maxResPreviewSize = size;
+      }
+    }
+
+    // If no exact match, use largest preview size. This was not a great idea on older devices because
+    // of the additional computation needed. We're likely to get here on newer Android 4+ devices, where
+    // the CPU is much more powerful.
+    if (maxResPreviewSize != null) {
+      Point largestSize = new Point(maxResPreviewSize.width, maxResPreviewSize.height);
+      Log.i(TAG, "Using largest suitable preview size: " + largestSize);
+      return largestSize;
+    }
+
+    // If there is nothing at all suitable, return current preview size
+    Camera.Size defaultPreview = parameters.getPreviewSize();
+    if (defaultPreview == null) {
+      throw new IllegalStateException("Parameters contained no preview size!");
+    }
+    Point defaultSize = new Point(defaultPreview.width, defaultPreview.height);
+    Log.i(TAG, "No suitable preview sizes, using default: " + defaultSize);
+    return defaultSize;
+  }
+
+  private static String findSettableValue(String name,
+                                          Collection<String> supportedValues,
+                                          String... desiredValues) {
+    Log.i(TAG, "Requesting " + name + " value from among: " + Arrays.toString(desiredValues));
+    Log.i(TAG, "Supported " + name + " values: " + supportedValues);
+    if (supportedValues != null) {
+      for (String desiredValue : desiredValues) {
+        if (supportedValues.contains(desiredValue)) {
+          Log.i(TAG, "Can set " + name + " to: " + desiredValue);
+          return desiredValue;
+        }
+      }
+    }
+    Log.i(TAG, "No supported values match");
+    return null;
+  }
+
+  private static String toString(Collection<int[]> arrays) {
+    if (arrays == null || arrays.isEmpty()) {
+      return "[]";
+    }
+    StringBuilder buffer = new StringBuilder();
+    buffer.append('[');
+    Iterator<int[]> it = arrays.iterator();
+    while (it.hasNext()) {
+      buffer.append(Arrays.toString(it.next()));
+      if (it.hasNext()) {
+        buffer.append(", ");
+      }
+    }
+    buffer.append(']');
+    return buffer.toString();
+  }
+
+  private static String toString(Iterable<Camera.Area> areas) {
+    if (areas == null) {
+      return null;
+    }
+    StringBuilder result = new StringBuilder();
+    for (Camera.Area area : areas) {
+      result.append(area.rect).append(':').append(area.weight).append(' ');
+    }
+    return result.toString();
+  }
+
+  public static String collectStats(Camera.Parameters parameters) {
+    return collectStats(parameters.flatten());
+  }
+
+  public static String collectStats(CharSequence flattenedParams) {
+    StringBuilder result = new StringBuilder(1000);
+
+    result.append("BOARD=").append(Build.BOARD).append('\n');
+    result.append("BRAND=").append(Build.BRAND).append('\n');
+    result.append("CPU_ABI=").append(Build.CPU_ABI).append('\n');
+    result.append("DEVICE=").append(Build.DEVICE).append('\n');
+    result.append("DISPLAY=").append(Build.DISPLAY).append('\n');
+    result.append("FINGERPRINT=").append(Build.FINGERPRINT).append('\n');
+    result.append("HOST=").append(Build.HOST).append('\n');
+    result.append("ID=").append(Build.ID).append('\n');
+    result.append("MANUFACTURER=").append(Build.MANUFACTURER).append('\n');
+    result.append("MODEL=").append(Build.MODEL).append('\n');
+    result.append("PRODUCT=").append(Build.PRODUCT).append('\n');
+    result.append("TAGS=").append(Build.TAGS).append('\n');
+    result.append("TIME=").append(Build.TIME).append('\n');
+    result.append("TYPE=").append(Build.TYPE).append('\n');
+    result.append("USER=").append(Build.USER).append('\n');
+    result.append("VERSION.CODENAME=").append(Build.VERSION.CODENAME).append('\n');
+    result.append("VERSION.INCREMENTAL=").append(Build.VERSION.INCREMENTAL).append('\n');
+    result.append("VERSION.RELEASE=").append(Build.VERSION.RELEASE).append('\n');
+    result.append("VERSION.SDK_INT=").append(Build.VERSION.SDK_INT).append('\n');
+
+    if (flattenedParams != null) {
+      String[] params = SEMICOLON.split(flattenedParams);
+      Arrays.sort(params);
+      for (String param : params) {
+        result.append(param).append('\n');
+      }
+    }
+
+    return result.toString();
+  }
+
+}

+ 352 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/CameraManager.java

@@ -0,0 +1,352 @@
+/*
+ * Copyright (C) 2008 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android.camera;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.Camera;
+import android.os.Handler;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.widget.ImageView;
+
+import com.google.zxing.PlanarYUVLuminanceSource;
+import com.google.zxing.client.android.camera.open.OpenCamera;
+import com.google.zxing.client.android.camera.open.OpenCameraInterface;
+import com.alva.flutter.plugin.scanner.R;
+
+import java.io.IOException;
+
+/**
+ * This object wraps the Camera service object and expects to be the only one talking to it. The
+ * implementation encapsulates the steps needed to take preview-sized images, which are used for
+ * both preview and decoding.
+ *
+ * @author dswitkin@google.com (Daniel Switkin)
+ */
+// camera APIs
+public final class CameraManager {
+
+  private static final String TAG = CameraManager.class.getSimpleName();
+
+  private static final int MIN_FRAME_WIDTH = 240;
+  private static final int MIN_FRAME_HEIGHT = 240;
+  private static final int MAX_FRAME_WIDTH = 720; // = 3/8 * 1920
+  private static final int MAX_FRAME_HEIGHT = 720; // = 2/3 * 1080
+
+  private final CameraConfigurationManager configManager;
+  private OpenCamera camera;
+  private AutoFocusManager autoFocusManager;
+  private Rect framingRect;
+  private Rect framingRectInPreview;
+  private boolean initialized;
+  private boolean previewing;
+  private int requestedCameraId = OpenCameraInterface.NO_REQUESTED_CAMERA;
+  private int requestedFramingRectWidth;
+  private int requestedFramingRectHeight;
+  /**
+   * Preview frames are delivered here, which we pass on to the registered handler. Make sure to
+   * clear the handler so it will only receive one message.
+   */
+  private final PreviewCallback previewCallback;
+
+  public CameraManager(Context context) {
+    this.configManager = new CameraConfigurationManager(context);
+    previewCallback = new PreviewCallback(configManager);
+  }
+
+  /**
+   * Opens the camera driver and initializes the hardware parameters.
+   *
+   * @param holder The surface object which the camera will draw preview frames into.
+   * @throws IOException Indicates the camera driver failed to open.
+   */
+  public synchronized void openDriver(SurfaceHolder holder) throws IOException {
+    OpenCamera theCamera = camera;
+    if (theCamera == null) {
+      theCamera = OpenCameraInterface.open(requestedCameraId);
+      if (theCamera == null) {
+        throw new IOException("Camera.open() failed to return object from driver");
+      }
+      camera = theCamera;
+    }
+
+    if (!initialized) {
+      initialized = true;
+      configManager.initFromCameraParameters(theCamera);
+      if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
+        setManualFramingRect(requestedFramingRectWidth, requestedFramingRectHeight);
+        requestedFramingRectWidth = 0;
+        requestedFramingRectHeight = 0;
+      }
+    }
+
+    Camera cameraObject = theCamera.getCamera();
+    Camera.Parameters parameters = cameraObject.getParameters();
+    String parametersFlattened = parameters == null ? null : parameters.flatten(); // Save these, temporarily
+    try {
+      configManager.setDesiredCameraParameters(theCamera, false);
+    } catch (RuntimeException re) {
+      // Driver failed
+      Log.w(TAG, "Camera rejected parameters. Setting only minimal safe-mode parameters");
+      Log.i(TAG, "Resetting to saved camera params: " + parametersFlattened);
+      // Reset:
+      if (parametersFlattened != null) {
+        parameters = cameraObject.getParameters();
+        parameters.unflatten(parametersFlattened);
+        try {
+          cameraObject.setParameters(parameters);
+          configManager.setDesiredCameraParameters(theCamera, true);
+        } catch (RuntimeException re2) {
+          // Well, darn. Give up
+          Log.w(TAG, "Camera rejected even safe-mode parameters! No configuration");
+        }
+      }
+    }
+    cameraObject.setPreviewDisplay(holder);
+
+  }
+
+  public synchronized boolean isOpen() {
+    return camera != null;
+  }
+
+  /**
+   * Closes the camera driver if still in use.
+   */
+  public synchronized void closeDriver() {
+    if (camera != null) {
+      camera.getCamera().release();
+      camera = null;
+      // Make sure to clear these each time we close the camera, so that any scanning rect
+      // requested by intent is forgotten.
+      framingRect = null;
+      framingRectInPreview = null;
+    }
+  }
+
+  /**
+   * Asks the camera hardware to begin drawing preview frames to the screen.
+   */
+  public synchronized void startPreview() {
+    OpenCamera theCamera = camera;
+    if (theCamera != null && !previewing) {
+      theCamera.getCamera().startPreview();
+      previewing = true;
+      autoFocusManager = new AutoFocusManager(theCamera.getCamera());
+    }
+  }
+
+  /**
+   * Tells the camera to stop drawing preview frames.
+   */
+  public synchronized void stopPreview() {
+    if (autoFocusManager != null) {
+      autoFocusManager.stop();
+      autoFocusManager = null;
+    }
+    if (camera != null && previewing) {
+      camera.getCamera().stopPreview();
+      previewCallback.setHandler(null, 0);
+      previewing = false;
+    }
+  }
+
+  /**
+   * Convenience method for {@link com.google.zxing.client.android.CaptureActivity}
+   *
+   * @param newSetting if {@code true}, light should be turned on if currently off. And vice versa.
+   */
+  public synchronized void setTorch(boolean newSetting) {
+    OpenCamera theCamera = camera;
+    if (theCamera != null && newSetting != configManager.getTorchState(theCamera.getCamera())) {
+      boolean wasAutoFocusManager = autoFocusManager != null;
+      if (wasAutoFocusManager) {
+        autoFocusManager.stop();
+        autoFocusManager = null;
+      }
+      configManager.setTorch(theCamera.getCamera(), newSetting);
+      if (wasAutoFocusManager) {
+        autoFocusManager = new AutoFocusManager(theCamera.getCamera());
+        autoFocusManager.start();
+      }
+    }
+  }
+
+  /**
+   * A single preview frame will be returned to the handler supplied. The data will arrive as byte[]
+   * in the message.obj field, with width and height encoded as message.arg1 and message.arg2,
+   * respectively.
+   *
+   * @param handler The handler to send the message to.
+   * @param message The what field of the message to be sent.
+   */
+  public synchronized void requestPreviewFrame(Handler handler, int message) {
+    OpenCamera theCamera = camera;
+    if (theCamera != null && previewing) {
+      previewCallback.setHandler(handler, message);
+      theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
+    }
+  }
+
+  /**
+   * Calculates the framing rect which the UI should draw to show the user where to place the
+   * barcode. This target helps with alignment as well as forces the user to hold the device
+   * far enough away to ensure the image will be in focus.
+   *
+   * @return The rectangle to draw on screen in window coordinates.
+   */
+  public synchronized Rect getFramingRect() {
+    if (framingRect == null) {
+      if (camera == null) {
+        return null;
+      }
+      Point screenResolution = configManager.getScreenResolution();
+      if (screenResolution == null) {
+        // Called early, before init even finished
+        return null;
+      }
+
+      int width = findDesiredDimensionInRange(screenResolution.x, MIN_FRAME_WIDTH, MAX_FRAME_WIDTH);
+      int height = findDesiredDimensionInRange(screenResolution.y, MIN_FRAME_HEIGHT, MAX_FRAME_HEIGHT);
+
+      int leftOffset = (screenResolution.x - width) / 2;
+      int topOffset = (screenResolution.y - height) / 2;
+      framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
+      Log.d(TAG, "Calculated framing rect: " + framingRect);
+    }
+    return framingRect;
+  }
+
+  private static int findDesiredDimensionInRange(int resolution, int hardMin, int hardMax) {
+    int dim = 2 * resolution / 3; // Target 2/3 of each dimension
+    if (dim < hardMin) {
+      return hardMin;
+    }
+    return Math.min(dim, hardMax);
+  }
+
+  /**
+   * Like {@link #getFramingRect} but coordinates are in terms of the preview frame,
+   * not UI / screen.
+   *
+   * @return {@link Rect} expressing barcode scan area in terms of the preview size
+   */
+  public synchronized Rect getFramingRectInPreview() {
+    if (framingRectInPreview == null) {
+      Rect framingRect = getFramingRect();
+      if (framingRect == null) {
+        return null;
+      }
+      Rect rect = new Rect(framingRect);
+      Point cameraResolution = configManager.getCameraResolution();
+      Point screenResolution = configManager.getScreenResolution();
+      if (cameraResolution == null || screenResolution == null) {
+        // Called early, before init even finished
+        return null;
+      }
+      if(screenResolution.x < screenResolution.y){
+        rect.left = rect.left * cameraResolution.y / screenResolution.x;
+        rect.right = rect.right * cameraResolution.y / screenResolution.x;
+        rect.top = rect.top * cameraResolution.x / screenResolution.y;
+        rect.bottom = rect.bottom * cameraResolution.x / screenResolution.y;
+      } else {
+        rect.left = rect.left * cameraResolution.x / screenResolution.x;
+        rect.right = rect.right * cameraResolution.x / screenResolution.x;
+        rect.top = rect.top * cameraResolution.y / screenResolution.y;
+        rect.bottom = rect.bottom * cameraResolution.y / screenResolution.y;
+      }
+      framingRectInPreview = rect;
+    }
+    return framingRectInPreview;
+  }
+
+
+  /**
+   * Allows third party apps to specify the camera ID, rather than determine
+   * it automatically based on available cameras and their orientation.
+   *
+   * @param cameraId camera ID of the camera to use. A negative value means "no preference".
+   */
+  public synchronized void setManualCameraId(int cameraId) {
+    requestedCameraId = cameraId;
+  }
+
+  /**
+   * Allows third party apps to specify the scanning rectangle dimensions, rather than determine
+   * them automatically based on screen resolution.
+   *
+   * @param width The width in pixels to scan.
+   * @param height The height in pixels to scan.
+   */
+  public synchronized void setManualFramingRect(int width, int height) {
+    if (initialized) {
+      Point screenResolution = configManager.getScreenResolution();
+      if (width > screenResolution.x) {
+        width = screenResolution.x;
+      }
+      if (height > screenResolution.y) {
+        height = screenResolution.y;
+      }
+      int leftOffset = (screenResolution.x - width) / 2;
+      int topOffset = (screenResolution.y - height) / 2;
+      framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
+      Log.d(TAG, "Calculated manual framing rect: " + framingRect);
+      framingRectInPreview = null;
+    } else {
+      requestedFramingRectWidth = width;
+      requestedFramingRectHeight = height;
+    }
+  }
+
+  /**
+   * A factory method to build the appropriate LuminanceSource object based on the format
+   * of the preview buffers, as described by Camera.Parameters.
+   *
+   * @param data A preview frame.
+   * @param width The width of the image.
+   * @param height The height of the image.
+   * @return A PlanarYUVLuminanceSource instance.
+   */
+  public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) {
+    Rect rect = getFramingRectInPreview();
+    if (rect == null) {
+      return null;
+    }
+    Point screenResolution = configManager.getScreenResolution();
+    if (screenResolution == null) {
+      // Called early, before init even finished
+      return null;
+    }
+    if(screenResolution.x < screenResolution.y){
+      byte[] rotatedData = new byte[data.length];
+      for (int y = 0; y < height; y++) {
+        for (int x = 0; x < width; x++)
+          rotatedData[x * height + height - y - 1] = data[x + y * width];
+      }
+      width = width ^ height;
+      height = height ^ width;
+      width = width ^ height;
+      data = rotatedData;
+    }
+    // Go ahead and assume it's YUV rather than die.
+    return new PlanarYUVLuminanceSource(data, width, height, rect.left, rect.top,
+            rect.width(), rect.height(), false);
+  }
+
+}

+ 57 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/PreviewCallback.java

@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2010 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android.camera;
+
+import android.graphics.Point;
+import android.hardware.Camera;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+@SuppressWarnings("deprecation") // camera APIs
+final class PreviewCallback implements Camera.PreviewCallback {
+
+  private static final String TAG = PreviewCallback.class.getSimpleName();
+
+  private final CameraConfigurationManager configManager;
+  private Handler previewHandler;
+  private int previewMessage;
+
+  PreviewCallback(CameraConfigurationManager configManager) {
+    this.configManager = configManager;
+  }
+
+  void setHandler(Handler previewHandler, int previewMessage) {
+    this.previewHandler = previewHandler;
+    this.previewMessage = previewMessage;
+  }
+
+  @Override
+  public void onPreviewFrame(byte[] data, Camera camera) {
+    Point cameraResolution = configManager.getCameraResolution();
+    Handler thePreviewHandler = previewHandler;
+    if (cameraResolution != null && thePreviewHandler != null) {
+      Message message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.x,
+          cameraResolution.y, data);
+      message.sendToTarget();
+      previewHandler = null;
+    } else {
+      Log.d(TAG, "Got preview callback, but no handler or resolution available");
+    }
+  }
+
+}

+ 27 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/open/CameraFacing.java

@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2015 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android.camera.open;
+
+/**
+ * Enumeration of directions a camera may face: front or back.
+ */
+public enum CameraFacing {
+
+  BACK,  // must be value 0!
+  FRONT, // must be value 1!
+
+}

+ 56 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/open/OpenCamera.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android.camera.open;
+
+import android.hardware.Camera;
+
+/**
+ * Represents an open {@link Camera} and its metadata, like facing direction and orientation.
+ */
+@SuppressWarnings("deprecation") // camera APIs
+public final class OpenCamera {
+  
+  private final int index;
+  private final Camera camera;
+  private final CameraFacing facing;
+  private final int orientation;
+  
+  public OpenCamera(int index, Camera camera, CameraFacing facing, int orientation) {
+    this.index = index;
+    this.camera = camera;
+    this.facing = facing;
+    this.orientation = orientation;
+  }
+
+  public Camera getCamera() {
+    return camera;
+  }
+
+  public CameraFacing getFacing() {
+    return facing;
+  }
+
+  public int getOrientation() {
+    return orientation;
+  }
+
+  @Override
+  public String toString() {
+    return "Camera #" + index + " : " + facing + ',' + orientation;
+  }
+
+}

+ 85 - 0
plugin/qrscanner/android/src/main/java/com/google/zxing/client/android/camera/open/OpenCameraInterface.java

@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2012 ZXing authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.zxing.client.android.camera.open;
+
+import android.hardware.Camera;
+import android.util.Log;
+
+/**
+ * Abstraction over the {@link Camera} API that helps open them and return their metadata.
+ */
+@SuppressWarnings("deprecation") // camera APIs
+public final class OpenCameraInterface {
+
+  private static final String TAG = OpenCameraInterface.class.getName();
+
+  /** For {@link #open(int)}, means no preference for which camera to open. */
+  public static final int NO_REQUESTED_CAMERA = -1;
+
+  private OpenCameraInterface() {
+  }
+
+  /**
+   * Opens the requested camera with {@link Camera#open(int)}, if one exists.
+   *
+   * @param cameraId camera ID of the camera to use. A negative value
+   *  or {@link #NO_REQUESTED_CAMERA} means "no preference", in which case a rear-facing
+   *  camera is returned if possible or else any camera
+   * @return handle to {@link OpenCamera} that was opened
+   */
+  public static OpenCamera open(int cameraId) {
+
+    int numCameras = Camera.getNumberOfCameras();
+    if (numCameras == 0) {
+      Log.w(TAG, "No cameras!");
+      return null;
+    }
+    if (cameraId >= numCameras) {
+      Log.w(TAG, "Requested camera does not exist: " + cameraId);
+      return null;
+    }
+
+    if (cameraId <= NO_REQUESTED_CAMERA) {
+      cameraId = 0;
+      while (cameraId < numCameras) {
+        Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
+        Camera.getCameraInfo(cameraId, cameraInfo);
+        if (CameraFacing.values()[cameraInfo.facing] == CameraFacing.BACK) {
+          break;
+        }
+        cameraId++;
+      }
+      if (cameraId == numCameras) {
+        Log.i(TAG, "No camera facing " + CameraFacing.BACK + "; returning camera #0");
+        cameraId = 0;
+      }
+    }
+
+    Log.i(TAG, "Opening camera #" + cameraId);
+    Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
+    Camera.getCameraInfo(cameraId, cameraInfo);
+    Camera camera = Camera.open(cameraId);
+    if (camera == null) {
+      return null;
+    }
+    return new OpenCamera(cameraId,
+                          camera,
+                          CameraFacing.values()[cameraInfo.facing],
+                          cameraInfo.orientation);
+  }
+
+}

+ 111 - 0
plugin/qrscanner/android/src/main/kotlin/com/alva/flutter/plugin/qrscanner/QrScannerPlugin.kt

@@ -0,0 +1,111 @@
+package com.alva.flutter.plugin.qrscanner
+
+import androidx.annotation.NonNull;
+
+import android.app.Activity
+import android.content.Intent
+import android.graphics.Color
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler
+import io.flutter.plugin.common.MethodChannel.Result
+import io.flutter.plugin.common.PluginRegistry
+import io.flutter.plugin.common.PluginRegistry.Registrar
+import io.flutter.embedding.engine.plugins.activity.ActivityAware
+import com.google.zxing.client.android.CaptureActivity
+import com.google.zxing.client.android.Intents
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
+
+/** QrScannerPlugin */
+public class QrScannerPlugin: FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener {
+  /// The MethodChannel that will the communication between Flutter and native Android
+  ///
+  /// This local reference serves to register the plugin with the Flutter Engine and unregister it
+  /// when the Flutter Engine is detached from the Activity
+  private lateinit var channel: MethodChannel
+  private lateinit var activity: Activity
+  private var result: Result? = null
+
+  private val requestCode: Int = 0
+
+  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+    channel = MethodChannel(flutterPluginBinding.flutterEngine.dartExecutor, "scanner")
+    channel.setMethodCallHandler(this);
+  }
+
+  // This static function is optional and equivalent to onAttachedToEngine. It supports the old
+  // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
+  // plugin registration via this function while apps migrate to use the new Android APIs
+  // post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
+  //
+  // It is encouraged to share logic between onAttachedToEngine and registerWith to keep
+  // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
+  // depending on the user's project. onAttachedToEngine or registerWith must both be defined
+  // in the same class.
+  companion object {
+    @JvmStatic
+    fun registerWith(registrar: Registrar) {
+      val channel = MethodChannel(registrar.messenger(), "scanner")
+      val instance = QrScannerPlugin()
+      instance.activity = registrar.activity()
+      registrar.addActivityResultListener(instance)
+      channel.setMethodCallHandler(instance)
+    }
+  }
+
+  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
+    if (call.method == "scan") {
+      val intent = Intent(activity, CaptureActivity::class.java)
+      intent.action = Intents.Scan.ACTION;
+      if(call.hasArgument(Intents.Scan.SCAN_TITLE))
+        intent.putExtra(Intents.Scan.SCAN_TITLE, call.argument<String>(Intents.Scan.SCAN_TITLE))
+      else
+        intent.putExtra(Intents.Scan.SCAN_TITLE, "二维码扫描")
+      if(call.hasArgument(Intents.Scan.LASER_COLOR))
+        intent.putExtra(Intents.Scan.LASER_COLOR, Color.parseColor(call.argument<String>(Intents.Scan.LASER_COLOR)))
+      if(call.hasArgument(Intents.Scan.KEY_PLAY_BEEP))
+        intent.putExtra(Intents.Scan.KEY_PLAY_BEEP, call.argument<Boolean>(Intents.Scan.KEY_PLAY_BEEP))
+      if(call.hasArgument(Intents.Scan.PROMPT_MESSAGE))
+        intent.putExtra(Intents.Scan.PROMPT_MESSAGE, call.argument<String>(Intents.Scan.PROMPT_MESSAGE))
+      if(call.hasArgument(Intents.Scan.ERROR_MESSAGE))
+        intent.putExtra(Intents.Scan.ERROR_MESSAGE, call.argument<String>(Intents.Scan.ERROR_MESSAGE))
+      if(call.hasArgument(Intents.Scan.PERMISSION_DENIED_MESSAGE))
+        intent.putExtra(Intents.Scan.PERMISSION_DENIED_MESSAGE, call.argument<String>(Intents.Scan.PERMISSION_DENIED_MESSAGE))
+      if(call.hasArgument(Intents.Scan.MESSAGE_CONFIRM_TEXT))
+        intent.putExtra(Intents.Scan.MESSAGE_CONFIRM_TEXT, call.argument<String>(Intents.Scan.MESSAGE_CONFIRM_TEXT))
+      if(call.hasArgument(Intents.Scan.MESSAGE_CANCEL_TEXT))
+        intent.putExtra(Intents.Scan.MESSAGE_CANCEL_TEXT, call.argument<String>(Intents.Scan.MESSAGE_CANCEL_TEXT))
+      activity.startActivityForResult(intent, requestCode)
+      this.result = result
+    } else {
+      result.notImplemented()
+    }
+  }
+
+  override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
+    channel.setMethodCallHandler(null)
+  }
+
+  override fun onActivityResult(requestCode: Int, resultCode: Int, data: android.content.Intent?): Boolean {
+    if (requestCode == requestCode && resultCode == Activity.RESULT_OK) {
+      result?.success(data?.getStringExtra(Intent.EXTRA_TEXT) ?: "")
+      return true
+    }
+    return false
+  }
+
+  override fun onAttachedToActivity(binding: ActivityPluginBinding) {
+    activity = binding.activity
+    binding.addActivityResultListener(this)
+  }
+
+  override fun onDetachedFromActivity() {
+  }
+
+  override fun onDetachedFromActivityForConfigChanges() {
+  }
+
+  override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
+  }
+}

BIN
plugin/qrscanner/android/src/main/res/drawable-xhdpi/btn_arrow_back.png


BIN
plugin/qrscanner/android/src/main/res/drawable-xhdpi/scan_finder_net.png


+ 76 - 0
plugin/qrscanner/android/src/main/res/layout/capture.xml

@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (C) 2008 ZXing authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+  <SurfaceView android:id="@+id/preview_view"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"/>
+
+  <com.google.zxing.client.android.ViewfinderView
+      android:id="@+id/viewfinder_view"
+      android:layout_width="match_parent"
+      android:layout_height="match_parent"/>
+
+  <androidx.constraintlayout.widget.ConstraintLayout
+      android:layout_width="match_parent"
+      android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/status_view"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@android:color/transparent"
+        android:textColor="@android:color/darker_gray"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        app:layout_constraintVertical_bias="0.9"
+        app:layout_constraintHorizontal_bias="0.5"/>
+  </androidx.constraintlayout.widget.ConstraintLayout>
+
+  <FrameLayout
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content"
+      android:fitsSystemWindows="true"
+      android:clipToPadding="true">
+    <FrameLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:onClick="onButtonClick"
+        android:padding="12dp">
+
+      <ImageView
+          android:layout_width="wrap_content"
+          android:layout_height="wrap_content"
+          android:contentDescription="@string/scanner_description"
+          android:background="@drawable/btn_arrow_back"/>
+    </FrameLayout>
+
+    <TextView
+        android:id="@+id/title"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center|center_vertical"
+        android:textColor="@android:color/white"
+        android:textSize="16sp" />
+  </FrameLayout>
+
+</FrameLayout>

BIN
plugin/qrscanner/android/src/main/res/raw/beep.ogg


+ 5 - 0
plugin/qrscanner/android/src/main/res/values/colors.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+  <color name="viewfinder_laser">#ffFF55FF</color> <!-- Android standard ICS color -->
+  <color name="viewfinder_mask">#60000000</color>
+</resources>

+ 9 - 0
plugin/qrscanner/android/src/main/res/values/strings.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<resources>
+  <string name="scanner_description">image</string>
+  <string name="scanner_button_ok">确定</string>
+  <string name="scanner_button_cancel">取消</string>
+  <string name="scanner_msg_framework_bug">哎呀,好像迷路了!您可能需要检查相机权限或重启设备。</string>
+  <string name="scanner_msg_status">请将取景框对准条码完成扫描</string>
+  <string name="scanner_msg_permission_denied">您的隐私设置似乎阻止我们访问您的相机进行条码扫描。 您可以通过执行以下操作进行修复,点击下面的确定按钮以打开设置,然后打开相机权限。</string>
+</resources>

+ 7 - 0
plugin/qrscanner/android/src/main/res/values/styles.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="ScannerTheme" parent="@android:style/Theme.Black.NoTitleBar">
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowActionBar">false</item>
+    </style>
+</resources>

+ 35 - 0
plugin/qrscanner/ios/Assets/FlutterQrScanner.xib

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
+    <device id="retina6_1" orientation="portrait" appearance="light"/>
+    <dependencies>
+        <deployment version="2304" identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16097"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <objects>
+        <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ScannerViewController">
+            <connections>
+                <outlet property="camera" destination="Ae3-e9-p2L" id="arR-A4-GKl"/>
+                <outlet property="view" destination="iN0-l3-epB" id="vk5-Bg-n45"/>
+            </connections>
+        </placeholder>
+        <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
+        <view contentMode="scaleToFill" id="iN0-l3-epB">
+            <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
+            <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+            <subviews>
+                <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ae3-e9-p2L">
+                    <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
+                    <color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                </view>
+            </subviews>
+            <constraints>
+                <constraint firstItem="Ae3-e9-p2L" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" id="4fj-0O-8Uy"/>
+                <constraint firstItem="Ae3-e9-p2L" firstAttribute="bottom" secondItem="iN0-l3-epB" secondAttribute="bottom" id="Gen-QV-VZq"/>
+                <constraint firstItem="Ae3-e9-p2L" firstAttribute="trailing" secondItem="iN0-l3-epB" secondAttribute="trailing" id="scd-ed-sBE"/>
+                <constraint firstItem="Ae3-e9-p2L" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="taz-nu-6Po"/>
+            </constraints>
+            <point key="canvasLocation" x="33.333333333333336" y="52.901785714285715"/>
+        </view>
+    </objects>
+</document>

BIN
plugin/qrscanner/ios/Assets/arrow_dark_left.png


BIN
plugin/qrscanner/ios/Assets/arrow_dark_left@2x.png


BIN
plugin/qrscanner/ios/Assets/arrow_left.png


BIN
plugin/qrscanner/ios/Assets/arrow_left@2x.png


BIN
plugin/qrscanner/ios/Assets/scannet.png


+ 4 - 0
plugin/qrscanner/ios/Classes/QrScannerPlugin.h

@@ -0,0 +1,4 @@
+#import <Flutter/Flutter.h>
+
+@interface QrScannerPlugin : NSObject<FlutterPlugin>
+@end

+ 15 - 0
plugin/qrscanner/ios/Classes/QrScannerPlugin.m

@@ -0,0 +1,15 @@
+#import "QrScannerPlugin.h"
+#if __has_include(<qrscanner/qrscanner-Swift.h>)
+#import <qrscanner/qrscanner-Swift.h>
+#else
+// Support project import fallback if the generated compatibility header
+// is not copied when this plugin is created as a library.
+// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816
+#import "qrscanner-Swift.h"
+#endif
+
+@implementation QrScannerPlugin
++ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
+  [SwiftQrScannerPlugin registerWithRegistrar:registrar];
+}
+@end

+ 479 - 0
plugin/qrscanner/ios/Classes/ScannerViewController.swift

@@ -0,0 +1,479 @@
+//
+//  ScannerViewController.swift
+//  scanner
+//
+//  Created by Alva on 2020/5/25.
+//  Copyright © 2020 Alva. All rights reserved.
+//
+
+import SwiftUI
+import AVFoundation
+
+var SCREENWidth = UIScreen.main.bounds.size.width
+var SCREENHeight = UIScreen.main.bounds.size.height
+let QRCodeWidth = Double(min(SCREENWidth, SCREENHeight)) / 1.5
+let RATIO = 0.45
+
+enum ArgumentsEnum: String {
+  case title = "SCAN_TITLE"
+  case laserColor = "LASER_COLOR"
+  case titleColor = "TITLE_COLOR"
+  case playBeep = "KEY_PLAY_BEEP"
+  case scanWidth = "SCAN_WIDTH"
+  case scanHeight = "SCAN_HEIGHT"
+  case promptMessage = "PROMPT_MESSAGE"
+  case permissionDeniedMessage = "PERMISSION_DENIED_MESSAGE"
+  case confirmText = "MESSAGE_CONFIRM_TEXT"
+  case cancelText = "MESSAGE_CANCEL_TEXT"
+  func getKeyValue<T>(dictionary: NSDictionary) -> T? {
+    guard let result =  dictionary[self.rawValue] as? T else {
+      return nil
+    }
+    return result
+  }
+}
+
+protocol ScannerDelegate: class {
+  func didScanWithResult(code: String)
+  func didFailWithErrorCode(code: String)
+}
+
+class ScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
+
+  @IBOutlet private var camera: UIView!
+  private var session: AVCaptureSession? = nil
+  private var top = 0.0
+
+  var bundle: Bundle? = nil
+
+  var currentOrientation = UIInterfaceOrientationMask.portrait
+
+  weak var delegate: ScannerDelegate?
+
+  var arguments: NSDictionary = [:]
+  var laserColor: UIColor = UIColor.clear
+  var promptMessage: String?
+  var permissionDeniedText: String = "Your privacy settings seem to prevent us from accessing your camera for barcode scanning. You can fix it by doing this, touch the OK button below to open the Settings and then turn the Camera on."
+  var confirmText: String = "OK"
+  var cancelText: String = "Cancel"
+
+  var windowOrientation: UIInterfaceOrientation {
+    if #available(iOS 13.0, *) {
+      return view.window?.windowScene?.interfaceOrientation ?? .unknown
+    } else {
+      return UIApplication.shared.statusBarOrientation
+    }
+  }
+
+  required init?(coder aDecoder: NSCoder) {
+    super.init(coder: aDecoder)
+  }
+
+  init() {
+    super.init(nibName: nil, bundle: nil)
+    let mainBundle = Bundle(for: type(of: self))
+    let url = mainBundle.url(forResource: "FlutterScannerBundle", withExtension: "bundle")
+
+    if let url = url {
+      bundle = Bundle(url: url)
+    }
+    if(bundle == nil) {
+      return
+    }
+    getCurrentOrientation()
+  }
+
+  override func viewDidLoad() {
+    super.viewDidLoad()
+    initArguments()
+    setupNavigationBar()
+  }
+
+  override func viewWillAppear(_ animated: Bool) {
+    super.viewWillAppear(animated)
+    checkAuthorization()
+  }
+
+  override var shouldAutorotate: Bool {
+    get {
+      return false
+    }
+  }
+
+  override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
+    get {
+      return currentOrientation
+    }
+  }
+
+  override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
+    session?.stopRunning()
+
+    SCREENWidth = UIScreen.main.bounds.size.width
+    SCREENHeight = UIScreen.main.bounds.size.height
+    checkAuthorization()
+  }
+
+  func getCurrentOrientation(){
+    switch UIDevice.current.orientation {
+    case UIDeviceOrientation.faceDown:
+      break
+    case UIDeviceOrientation.unknown:
+      break
+    case UIDeviceOrientation.portrait:
+      break
+    case UIDeviceOrientation.portraitUpsideDown:
+      break
+    case UIDeviceOrientation.faceUp:
+      break
+    case UIDeviceOrientation.landscapeLeft:
+      currentOrientation = UIInterfaceOrientationMask.landscapeRight
+      break
+    case UIDeviceOrientation.landscapeRight:
+      currentOrientation = UIInterfaceOrientationMask.landscapeLeft
+      break
+    @unknown default:
+      break
+    }
+  }
+
+  func initArguments() {
+    SCREENWidth = UIScreen.main.bounds.size.width
+    SCREENHeight = UIScreen.main.bounds.size.height
+
+    if let titleHex: String = ArgumentsEnum.title.getKeyValue(dictionary: arguments) {
+      self.title = titleHex
+    }
+    if let laserColorHex: String = ArgumentsEnum.laserColor.getKeyValue(dictionary: arguments) {
+      laserColor = UIColor(hexString: laserColorHex) ?? UIColor.clear
+    }
+    if let promptMessageHex: String = ArgumentsEnum.promptMessage.getKeyValue(dictionary: arguments) {
+      promptMessage = promptMessageHex
+    }
+    if let permissionDeniedTextHex: String = ArgumentsEnum.permissionDeniedMessage.getKeyValue(dictionary: arguments) {
+      permissionDeniedText = permissionDeniedTextHex
+    }
+    if let confirmTextHex: String = ArgumentsEnum.confirmText.getKeyValue(dictionary: arguments) {
+      confirmText = confirmTextHex
+    }
+    if let cancelTextHex: String = ArgumentsEnum.cancelText.getKeyValue(dictionary: arguments) {
+      cancelText = cancelTextHex
+    }
+  }
+
+  func checkAuthorization(){
+    /*
+    Check the video authorization status. Video access is required and audio
+    access is optional. If the user denies audio access, AVCam won't
+    record audio during movie recording.
+    */
+    switch AVCaptureDevice.authorizationStatus(for: .video) {
+      case .authorized:
+        self.setupMaskView()
+        self.beginScanning()
+        break
+      case .notDetermined:
+        AVCaptureDevice.requestAccess(for: .video, completionHandler: { granted in
+          if !granted {
+            self.permissionDenied()
+          }
+          self.setupMaskView()
+          self.beginScanning()
+        })
+      default:
+        self.permissionDenied()
+    }
+  }
+
+  func permissionDenied(){
+    DispatchQueue.main.async {
+      let alertController = UIAlertController(title: self.title, message: self.permissionDeniedText, preferredStyle: .alert)
+      let confirmAction = UIAlertAction(title: self.confirmText, style: .default) { (action) in
+        if let url = URL(string: UIApplication.openSettingsURLString) {
+          if #available(iOS 10, *) {
+            UIApplication.shared.open(url, options: [:], completionHandler: {
+              (success) in
+                self.dismiss(animated: true, completion: nil)
+            })
+          } else {
+            UIApplication.shared.openURL(url)
+            self.dismiss(animated: true, completion: nil)
+          }
+        }
+      }
+      let cancelAction = UIAlertAction(title: self.cancelText, style: .default) { (action) in
+        self.dismiss(animated: true, completion: nil)
+      }
+      alertController.addAction(confirmAction)
+      alertController.addAction(cancelAction)
+      self.present(alertController, animated: true)
+    }
+  }
+
+  func setupMaskView() {
+    UINib.init(nibName: "FlutterQrScanner", bundle: bundle!).instantiate(withOwner: self, options: nil)
+
+    var scanY = Double(SCREENHeight) - top - QRCodeWidth
+    scanY = scanY * RATIO
+    let frame = CGRect(x: (Double(SCREENWidth) - QRCodeWidth) / 2.0, y: scanY, width: QRCodeWidth, height: QRCodeWidth)
+
+    let backgroundView = UIView(frame: UIScreen.main.bounds)
+    backgroundView.backgroundColor =  UIColor.init(red: 0, green: 0, blue: 0, alpha: 0.6)
+    camera.addSubview(backgroundView)
+
+    let maskLayer = CAShapeLayer()
+    maskLayer.fillRule = CAShapeLayerFillRule.evenOdd // fill rule
+    let basicPath = UIBezierPath(rect: UIScreen.main.bounds) // basic
+    let maskPath = UIBezierPath(roundedRect: frame, cornerRadius: 15)
+    basicPath.append(maskPath) // recover
+    maskLayer.path = basicPath.cgPath
+
+    backgroundView.layer.mask = maskLayer
+
+    let scanBorder = BorderCanvas(frame: frame, border: laserColor)
+    camera.addSubview(scanBorder)
+
+    let scanWindow = UIView(frame: frame)
+    scanWindow.clipsToBounds = true
+    camera.addSubview(scanWindow)
+
+    let winMaskLayer = CAShapeLayer()
+    // fill rule
+    winMaskLayer.fillRule = CAShapeLayerFillRule.evenOdd
+    let winFrame = CGRect(x: 0, y: 0, width: QRCodeWidth, height: QRCodeWidth)
+    let winBasicPath = UIBezierPath(rect: winFrame)
+    let winMaskPath = UIBezierPath(rect: winFrame)
+    let winMaskPath2 = UIBezierPath(roundedRect: winFrame, cornerRadius: 15)
+    winBasicPath.append(winMaskPath)
+    winBasicPath.append(winMaskPath2)
+    winMaskLayer.path = winBasicPath.cgPath
+
+    scanWindow.layer.mask = winMaskLayer
+
+    //scan window animation
+    let scanNetImageViewH = scanWindow.frame.size.height
+    let scanNetImageViewW = scanWindow.frame.size.width
+    let scanNetImageView = UIImageView(image: UIImage.init(named: "scannet", in: bundle!, compatibleWith: nil))
+    scanNetImageView.frame = CGRect(x: 0, y: -scanNetImageViewH, width: scanNetImageViewW, height: scanNetImageViewH)
+    let scanNetAnimation = CABasicAnimation(keyPath: "transform.translation.y")
+    scanNetAnimation.byValue = NSNumber(value: QRCodeWidth)
+    scanNetAnimation.duration = 1.5
+    scanNetAnimation.repeatCount = MAXFLOAT
+    scanNetImageView.layer.add(scanNetAnimation, forKey: "animation")
+    scanWindow.addSubview(scanNetImageView)
+
+    let promptSize = CGSize(width: Double(SCREENWidth) - 30, height: 0)
+    let promptRect = (promptMessage ?? "").boundingRect(with: promptSize, options: NSStringDrawingOptions.usesLineFragmentOrigin, attributes: nil , context: nil)
+    let promptLabel = UILabel(frame: CGRect(x: 15, y: scanY + QRCodeWidth + 30, width: Double(SCREENWidth) - 30, height: Double(promptRect.size.height)))
+    promptLabel.textColor = UIColor.gray
+    promptLabel.text = promptMessage
+    promptLabel.textAlignment = NSTextAlignment.center
+    camera.addSubview(promptLabel)
+  }
+
+  func setupNavigationBar(){
+    let navHeight = self.navigationController?.navigationBar.bounds.height ?? 20
+    let backButton = UIButton(frame: CGRect(x: 0, y: 0, width: navHeight, height: navHeight))
+    backButton.setImage(UIImage.init(named: "arrow_left", in: bundle!, compatibleWith: nil), for: .normal)
+    backButton.setTitle("", for: .normal)
+    backButton.setTitleColor(backButton.tintColor, for: .normal)
+    backButton.addTarget(self, action: #selector(backButtonPressed), for: .touchUpInside)
+    backButton.imageView?.contentMode = .scaleAspectFit
+    self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: backButton)
+
+    self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
+    self.navigationController?.navigationBar.shadowImage = UIImage()
+    self.navigationController?.navigationBar.isTranslucent = true
+    self.navigationController?.navigationBar.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white, NSAttributedString.Key.font: UIFont.systemFont(ofSize:18)]
+  }
+
+  func beginScanning() {
+    //get device
+    let device = AVCaptureDevice.default(for: .video)
+    //create device input
+    var input: AVCaptureDeviceInput? = nil
+    do {
+      if let device = device {
+          input = try AVCaptureDeviceInput(device: device)
+      }
+    } catch {
+      delegate?.didFailWithErrorCode(code: "")
+      return
+    }
+    if(input == nil){
+      delegate?.didFailWithErrorCode(code: "")
+      return
+    }
+    //create device output
+    let output = AVCaptureMetadataOutput()
+
+    let xx = (Double(SCREENHeight) - QRCodeWidth - top) * RATIO
+    let x = xx / Double(SCREENHeight)
+    let yy = (Double(SCREENWidth) - QRCodeWidth) / 2.0
+    let y = yy / Double(SCREENWidth)
+    let width = QRCodeWidth / Double(SCREENHeight)
+    let height = QRCodeWidth / Double(SCREENWidth)
+    output.rectOfInterest = CGRect(x: x, y: y, width: width, height: height)
+
+    output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
+
+    session = AVCaptureSession()
+    session!.sessionPreset = .high
+
+    if session?.canAddInput(input!) ?? false {
+      session!.addInput(input!)
+      session!.addOutput(output)
+
+      //code data type
+      output.metadataObjectTypes = [
+        .qr,
+        .ean13,
+        .ean8,
+        .code128
+      ]
+
+      let layer = AVCaptureVideoPreviewLayer(session: session!)
+      layer.frame = CGRect(x: 0, y: 0, width: CGFloat(SCREENWidth), height: SCREENHeight - CGFloat(top))
+      layer.videoGravity = .resizeAspectFill
+      camera.layer.insertSublayer(layer, at: 0)
+
+      DispatchQueue.main.async {
+        /*
+        Dispatch video streaming to the main queue because AVCaptureVideoPreviewLayer is the backing layer for PreviewView.
+        You can manipulate UIView only on the main thread.
+        Note: As an exception to the above rule, it's not necessary to serialize video orientation changes
+        on the AVCaptureVideoPreviewLayer’s connection with other session manipulation.
+
+        Use the window scene's orientation as the initial video orientation. Subsequent orientation changes are
+        handled by CameraViewController.viewWillTransition(to:with:).
+        */
+        var initialVideoOrientation: AVCaptureVideoOrientation = .portrait
+        if self.windowOrientation != .unknown {
+            if let videoOrientation = AVCaptureVideoOrientation(rawValue: self.windowOrientation.rawValue) {
+                initialVideoOrientation = videoOrientation
+            }
+        }
+        layer.connection?.videoOrientation = initialVideoOrientation
+      }
+
+      //start
+      session!.startRunning()
+    } else {
+       print("Couldn't add video device input to the session.")
+       session!.commitConfiguration()
+       return
+    }
+  }
+
+  func metadataOutput(_ captureOutput: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
+    if metadataObjects.count > 0 {
+      //code result data
+      let metadataObject = metadataObjects[0] as? AVMetadataMachineReadableCodeObject
+      delegate?.didScanWithResult(code: metadataObject?.stringValue ?? "")
+      self.dismiss(animated: true, completion: nil)
+    }
+  }
+
+  @objc func backButtonPressed() {
+    delegate?.didFailWithErrorCode(code: "canceled")
+    self.dismiss(animated: true, completion: nil)
+  }
+
+  override func viewWillDisappear(_ animated: Bool) {
+    super.viewWillDisappear(animated)
+    self.session?.stopRunning()
+  }
+}
+
+class BorderCanvas: UIView {
+  var border: UIColor = UIColor.clear
+
+  override init(frame: CGRect) {
+    super.init(frame: frame)
+    self.backgroundColor = UIColor.clear
+  }
+
+  convenience init(frame: CGRect, border: UIColor?) {
+    self.init(frame: frame)
+    self.border = border ?? UIColor.clear
+  }
+
+  required init?(coder aDecoder: NSCoder) {
+    fatalError("init(coder:) has not been implemented")
+  }
+
+  override func draw(_ rect: CGRect) {
+    let pathRect = self.bounds.insetBy(dx: 1, dy: 1)
+    let path = UIBezierPath(roundedRect: pathRect, cornerRadius: 15)
+    path.lineWidth = 3
+    UIColor.clear.setFill()
+    self.border.setStroke()
+    path.fill()
+    path.stroke()
+
+    let maskLayer = CAShapeLayer()
+    maskLayer.fillRule = CAShapeLayerFillRule.evenOdd // fill rule
+    let basicPath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: QRCodeWidth, height: QRCodeWidth)) // basic
+    let maskPath = UIBezierPath(rect: CGRect(x: QRCodeWidth / 6, y: 0, width: QRCodeWidth * 2 / 3, height: QRCodeWidth))
+    let maskPath2 = UIBezierPath(rect: CGRect(x: 0, y: QRCodeWidth / 6, width: QRCodeWidth, height: QRCodeWidth * 2 / 3))
+    basicPath.append(maskPath) // recover
+    basicPath.append(maskPath2) // recover
+    maskLayer.path = basicPath.cgPath
+
+    layer.mask = maskLayer
+  }
+}
+
+extension AVCaptureVideoOrientation {
+  init?(deviceOrientation: UIDeviceOrientation) {
+    switch deviceOrientation {
+      case .portrait: self = .portrait
+      case .portraitUpsideDown: self = .portraitUpsideDown
+      case .landscapeLeft: self = .landscapeRight
+      case .landscapeRight: self = .landscapeLeft
+      default: return nil
+    }
+  }
+
+  init?(interfaceOrientation: UIInterfaceOrientation) {
+    switch interfaceOrientation {
+      case .portrait: self = .portrait
+      case .portraitUpsideDown: self = .portraitUpsideDown
+      case .landscapeLeft: self = .landscapeLeft
+      case .landscapeRight: self = .landscapeRight
+      default: return nil
+    }
+  }
+}
+
+extension UIColor {
+  convenience init(rgba: Int) {
+    self.init(
+      red: CGFloat((rgba & 0x00FF0000) >> 16) / 255.0,
+      green: CGFloat((rgba & 0x0000FF00) >> 8) / 255.0,
+      blue: CGFloat(rgba & 0x000000FF) / 255.0,
+      alpha: CGFloat((rgba & 0xFF000000) >> 24) / 255.0
+    )
+  }
+
+  convenience init?(hexString: String) {
+    var chars = Array(hexString.hasPrefix("#") ? hexString.dropFirst() : hexString[...])
+    let red, green, blue, alpha: CGFloat
+    switch chars.count {
+    case 3:
+      chars = chars.flatMap { [$0, $0] }
+      fallthrough
+    case 6:
+      chars = ["F","F"] + chars
+      fallthrough
+    case 8:
+      alpha = CGFloat(strtoul(String(chars[0...1]), nil, 16)) / 255
+      red   = CGFloat(strtoul(String(chars[2...3]), nil, 16)) / 255
+      green = CGFloat(strtoul(String(chars[4...5]), nil, 16)) / 255
+      blue  = CGFloat(strtoul(String(chars[6...7]), nil, 16)) / 255
+    default:
+      return nil
+    }
+    self.init(red: red, green: green, blue:  blue, alpha: alpha)
+  }
+}

+ 72 - 0
plugin/qrscanner/ios/Classes/SwiftQrScannerPlugin.swift

@@ -0,0 +1,72 @@
+import Flutter
+import UIKit
+
+public class SwiftQrScannerPlugin: NSObject, FlutterPlugin {
+  var registrar: FlutterPluginRegistrar!
+
+  var viewController: UIViewController!
+  var result: FlutterResult?
+
+  public static func register(with registrar: FlutterPluginRegistrar) {
+    let channel = FlutterMethodChannel(name: "scanner", binaryMessenger: registrar.messenger())
+    let instance = SwiftQrScannerPlugin()
+    registrar.addMethodCallDelegate(instance, channel: channel)
+    if let delegate = UIApplication.shared.delegate, let window = delegate.window, let root = window?.rootViewController {
+      instance.viewController = root
+      instance.registrar = registrar
+    }
+  }
+
+  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+    switch call.method {
+      case "scan":
+        self.result = result
+        let scannerController = ScannerViewController()
+        scannerController.delegate = self
+        let navigationController = CSNavigationController(rootViewController: scannerController)
+        scannerController.modalPresentationStyle = .fullScreen
+        navigationController.modalPresentationStyle = .fullScreen
+
+        if let arguments = call.arguments as? NSDictionary {
+           scannerController.arguments = arguments
+        }
+
+        if viewController != nil {
+          viewController.present(navigationController, animated: true, completion: nil)
+        }
+      default:
+        result(FlutterMethodNotImplemented)
+        break
+     }
+  }
+}
+
+extension SwiftQrScannerPlugin: ScannerDelegate {
+  func didScanWithResult(code: String) {
+    if let channelResult = result {
+      channelResult(code as NSString)
+    }
+  }
+
+  func didFailWithErrorCode(code: String) {
+    if let channelResult = result {
+      channelResult("")
+    }
+  }
+}
+
+class CSNavigationController: UINavigationController {
+    override func viewDidLoad() {
+        super.viewDidLoad()
+    }
+
+    override var shouldAutorotate: Bool {
+        return self.topViewController?.shouldAutorotate ?? false
+    }
+
+    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
+      get {
+        return self.topViewController?.supportedInterfaceOrientations ?? UIInterfaceOrientationMask.portrait
+      }
+    }
+}

+ 24 - 0
plugin/qrscanner/ios/qrscanner.podspec

@@ -0,0 +1,24 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint qrscanner.podspec' to validate before publishing.
+#
+Pod::Spec.new do |s|
+  s.name             = 'qrscanner'
+  s.version          = '0.0.1'
+  s.summary          = 'A qrcode scanner flutter plugin project. Support iOS, Android.'
+  s.description      = <<-DESC
+A qrcode scanner flutter plugin project. Support iOS, Android.
+                       DESC
+  s.homepage         = 'http://example.com'
+  s.license          = { :file => '../LICENSE' }
+  s.author           = { 'Your Company' => 'email@example.com' }
+  s.source           = { :path => '.' }
+  s.source_files = 'Classes/**/*'
+  s.dependency 'Flutter'
+  s.platform = :ios, '8.0'
+  s.resource_bundles = { 'FlutterScannerBundle' => ['Assets/*.{xib,png}'] }
+
+  # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported.
+  s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
+  s.swift_version = '5.0'
+end

+ 49 - 0
plugin/qrscanner/lib/flutter_plugin_qr_scanner.dart

@@ -0,0 +1,49 @@
+import 'dart:async';
+import 'dart:ui';
+
+import 'package:flutter/services.dart';
+
+class QrScanner {
+  static const MethodChannel _channel = const MethodChannel('scanner');
+
+  static Future<String> scan({
+    String title = "",
+    Color laserColor,
+    bool playBeep = false, //android support
+    String promptMessage,
+    String errorMsg, //android support
+    String permissionDeniedText,
+    String messageConfirmText,
+    String messageCancelText,
+  }) async {
+    Map<String, dynamic> arguments = {
+      Intents.TITLE: title,
+      Intents.KEY_PLAY_BEEP: playBeep,
+    };
+    if (laserColor != null)
+      arguments[Intents.LASER_COLOR] = '#${laserColor.value.toRadixString(16)}';
+    if (promptMessage != null)
+      arguments[Intents.PROMPT_MESSAGE] = promptMessage;
+    if (errorMsg != null)
+      arguments[Intents.ERROR_MESSAGE] = errorMsg;
+    if (permissionDeniedText != null)
+      arguments[Intents.PERMISSION_DENIED_MESSAGE] = permissionDeniedText;
+    if (messageConfirmText != null)
+      arguments[Intents.MESSAGE_CONFIRM_TEXT] = messageConfirmText;
+    if (messageCancelText != null)
+      arguments[Intents.MESSAGE_CANCEL_TEXT] = messageCancelText;
+    final String code = await _channel.invokeMethod('scan', arguments);
+    return code;
+  }
+}
+
+class Intents {
+  static const TITLE = "SCAN_TITLE";
+  static const LASER_COLOR = "LASER_COLOR";
+  static const KEY_PLAY_BEEP = "KEY_PLAY_BEEP";
+  static const PROMPT_MESSAGE = "PROMPT_MESSAGE";
+  static const ERROR_MESSAGE = "ERROR_MESSAGE";
+  static const PERMISSION_DENIED_MESSAGE = "PERMISSION_DENIED_MESSAGE";
+  static const MESSAGE_CONFIRM_TEXT = "MESSAGE_CONFIRM_TEXT";
+  static const MESSAGE_CANCEL_TEXT = "MESSAGE_CANCEL_TEXT";
+}

+ 65 - 0
plugin/qrscanner/pubspec.yaml

@@ -0,0 +1,65 @@
+name: qrscanner
+description: A qrcode scanner flutter plugin project. Support iOS, Android.
+version: 0.0.1
+author: alvagan <alva.sky.gan@gmail.com>
+homepage: https://github.com/alvagan/flutter_plugin_qr_scanner
+
+environment:
+  sdk: ">=2.7.0 <3.0.0"
+  flutter: ">=1.10.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+
+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:
+  # This section identifies this Flutter project as a plugin project.
+  # The 'pluginClass' and Android 'package' identifiers should not ordinarily
+  # be modified. They are used by the tooling to maintain consistency when
+  # adding or updating assets for this project.
+  plugin:
+    platforms:
+      android:
+        package: com.alva.flutter.plugin.qrscanner
+        pluginClass: QrScannerPlugin
+      ios:
+        pluginClass: QrScannerPlugin
+
+  # To add assets to your plugin package, add an assets section, like this:
+  # assets:
+  #   - images/a_dot_burr.jpeg
+  #   - images/a_dot_ham.jpeg
+  #
+  # For details regarding assets in packages, see
+  # https://flutter.dev/assets-and-images/#from-packages
+  #
+  # An image asset can refer to one or more resolution-specific "variants", see
+  # https://flutter.dev/assets-and-images/#resolution-aware.
+
+  # To add custom fonts to your plugin package, 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 in packages, see
+  # https://flutter.dev/custom-fonts/#from-packages

+ 6 - 2
pubspec.yaml

@@ -61,7 +61,6 @@ dependencies:
   extended_nested_scroll_view: ^1.0.1
   flutter_spinkit: ^4.1.2 # 优秀的等待动画(菊花哥)
   flutter_swiper: ^1.1.6
-  percent_indicator: ^2.1.9
   flutter_rating_bar: ^3.2.0+1
   flutter_timeline: ^0.1.1+13
 
@@ -73,8 +72,9 @@ dependencies:
   path_provider: ^1.6.11 #路径
   package_info: any
   device_info: any
-  flutter_dong_scan: ^1.0.8 # 扫描二维码
   qr_flutter: ^3.2.0        # 生成二维码
+  qrscanner:   # 扫描二维码
+    path: plugin/qrscanner
   webview_flutter: ^0.3.24   # WebView
   amap_location_fluttify: ^0.17.0
 
@@ -134,3 +134,7 @@ flutter:
   assets:
     - lib/assets/img/       # Base 1.0
     - lib/assets/json/
+  fonts:
+     - family: DIN
+       fonts:
+         - asset: fonts/DIN-BOLD.OTF