Kaynağa Gözat

first commit

liuxiulin 1 yıl önce
işleme
bb5ad3f48a
100 değiştirilmiş dosya ile 9738 ekleme ve 0 silme
  1. 18 0
      .gitignore
  2. 3 0
      .idea/.gitignore
  3. 1 0
      .idea/.name
  4. 117 0
      .idea/codeStyles/Project.xml
  5. 5 0
      .idea/codeStyles/codeStyleConfig.xml
  6. 6 0
      .idea/compiler.xml
  7. 25 0
      .idea/deploymentTargetDropDown.xml
  8. 24 0
      .idea/gradle.xml
  9. 6 0
      .idea/kotlinc.xml
  10. 83 0
      .idea/misc.xml
  11. 6 0
      .idea/render.experimental.xml
  12. 6 0
      .idea/vcs.xml
  13. 31 0
      FastBleLib/build.gradle
  14. 17 0
      FastBleLib/proguard-rules.pro
  15. 42 0
      FastBleLib/src/main/AndroidManifest.xml
  16. 936 0
      FastBleLib/src/main/java/com/clj/fastble/BleManager.java
  17. 612 0
      FastBleLib/src/main/java/com/clj/fastble/bluetooth/BleBluetooth.java
  18. 608 0
      FastBleLib/src/main/java/com/clj/fastble/bluetooth/BleConnector.java
  19. 139 0
      FastBleLib/src/main/java/com/clj/fastble/bluetooth/MultipleBluetoothController.java
  20. 158 0
      FastBleLib/src/main/java/com/clj/fastble/bluetooth/SplitWriter.java
  21. 27 0
      FastBleLib/src/main/java/com/clj/fastble/callback/BleBaseCallback.java
  22. 24 0
      FastBleLib/src/main/java/com/clj/fastble/callback/BleGattCallback.java
  23. 13 0
      FastBleLib/src/main/java/com/clj/fastble/callback/BleIndicateCallback.java
  24. 12 0
      FastBleLib/src/main/java/com/clj/fastble/callback/BleMtuChangedCallback.java
  25. 14 0
      FastBleLib/src/main/java/com/clj/fastble/callback/BleNotifyCallback.java
  26. 12 0
      FastBleLib/src/main/java/com/clj/fastble/callback/BleReadCallback.java
  27. 12 0
      FastBleLib/src/main/java/com/clj/fastble/callback/BleRssiCallback.java
  28. 13 0
      FastBleLib/src/main/java/com/clj/fastble/callback/BleScanAndConnectCallback.java
  29. 14 0
      FastBleLib/src/main/java/com/clj/fastble/callback/BleScanCallback.java
  30. 11 0
      FastBleLib/src/main/java/com/clj/fastble/callback/BleScanPresenterImp.java
  31. 12 0
      FastBleLib/src/main/java/com/clj/fastble/callback/BleWriteCallback.java
  32. 29 0
      FastBleLib/src/main/java/com/clj/fastble/data/BleConnectStateParameter.java
  33. 112 0
      FastBleLib/src/main/java/com/clj/fastble/data/BleDevice.java
  34. 60 0
      FastBleLib/src/main/java/com/clj/fastble/data/BleMsg.java
  35. 18 0
      FastBleLib/src/main/java/com/clj/fastble/data/BleScanState.java
  36. 8 0
      FastBleLib/src/main/java/com/clj/fastble/data/BleWriteState.java
  37. 47 0
      FastBleLib/src/main/java/com/clj/fastble/exception/BleException.java
  38. 42 0
      FastBleLib/src/main/java/com/clj/fastble/exception/ConnectException.java
  39. 28 0
      FastBleLib/src/main/java/com/clj/fastble/exception/GattException.java
  40. 10 0
      FastBleLib/src/main/java/com/clj/fastble/exception/OtherException.java
  41. 10 0
      FastBleLib/src/main/java/com/clj/fastble/exception/TimeoutException.java
  42. 238 0
      FastBleLib/src/main/java/com/clj/fastble/scan/BleScanPresenter.java
  43. 109 0
      FastBleLib/src/main/java/com/clj/fastble/scan/BleScanRuleConfig.java
  44. 139 0
      FastBleLib/src/main/java/com/clj/fastble/scan/BleScanner.java
  45. 31 0
      FastBleLib/src/main/java/com/clj/fastble/utils/BleLog.java
  46. 34 0
      FastBleLib/src/main/java/com/clj/fastble/utils/BleLruHashMap.java
  47. 121 0
      FastBleLib/src/main/java/com/clj/fastble/utils/HexUtil.java
  48. 1 0
      RangeSeekBar/.gitignore
  49. 39 0
      RangeSeekBar/build.gradle
  50. 17 0
      RangeSeekBar/proguard-rules.pro
  51. 26 0
      RangeSeekBar/src/androidTest/java/com/jaygoo/widget/ExampleInstrumentedTest.java
  52. 11 0
      RangeSeekBar/src/main/AndroidManifest.xml
  53. 17 0
      RangeSeekBar/src/main/java/com/jaygoo/widget/OnRangeChangedListener.java
  54. 1241 0
      RangeSeekBar/src/main/java/com/jaygoo/widget/RangeSeekBar.java
  55. 57 0
      RangeSeekBar/src/main/java/com/jaygoo/widget/SavedState.java
  56. 691 0
      RangeSeekBar/src/main/java/com/jaygoo/widget/SeekBar.java
  57. 21 0
      RangeSeekBar/src/main/java/com/jaygoo/widget/SeekBarState.java
  58. 186 0
      RangeSeekBar/src/main/java/com/jaygoo/widget/Utils.java
  59. 278 0
      RangeSeekBar/src/main/java/com/jaygoo/widget/VerticalRangeSeekBar.java
  60. 170 0
      RangeSeekBar/src/main/java/com/jaygoo/widget/VerticalSeekBar.java
  61. 8 0
      RangeSeekBar/src/main/res/drawable/rsb_default_thumb.xml
  62. 141 0
      RangeSeekBar/src/main/res/values/attrs.xml
  63. 10 0
      RangeSeekBar/src/main/res/values/colors.xml
  64. 3 0
      RangeSeekBar/src/main/res/values/strings.xml
  65. 11 0
      RangeSeekBar/src/main/res/values/styles.xml
  66. 17 0
      RangeSeekBar/src/test/java/com/jaygoo/widget/ExampleUnitTest.java
  67. 1 0
      app/.gitignore
  68. 157 0
      app/build.gradle
  69. 21 0
      app/proguard-rules.pro
  70. 24 0
      app/src/androidTest/java/com/rdiot/yx485/ExampleInstrumentedTest.kt
  71. 127 0
      app/src/main/AndroidManifest.xml
  72. 156 0
      app/src/main/java/com/rdiot/yx485/MyApp.kt
  73. 62 0
      app/src/main/java/com/rdiot/yx485/adapter/CustomDataBindAdapter.kt
  74. 72 0
      app/src/main/java/com/rdiot/yx485/adapter/FamilyAdapter.kt
  75. 157 0
      app/src/main/java/com/rdiot/yx485/adapter/RoomsAdapter.kt
  76. 111 0
      app/src/main/java/com/rdiot/yx485/adapter/ScenesAdapter.kt
  77. 152 0
      app/src/main/java/com/rdiot/yx485/base/AppManager.kt
  78. 65 0
      app/src/main/java/com/rdiot/yx485/base/BaseActivity.kt
  79. 59 0
      app/src/main/java/com/rdiot/yx485/base/BaseFragment.kt
  80. 46 0
      app/src/main/java/com/rdiot/yx485/base/BaseMainPagerAdapter.kt
  81. 54 0
      app/src/main/java/com/rdiot/yx485/base/BaseViewModelFactory.kt
  82. 89 0
      app/src/main/java/com/rdiot/yx485/base/LocalData.kt
  83. 34 0
      app/src/main/java/com/rdiot/yx485/bean/AppVersionData.kt
  84. 26 0
      app/src/main/java/com/rdiot/yx485/bean/AvatarData.kt
  85. 19 0
      app/src/main/java/com/rdiot/yx485/bean/BaseResp.kt
  86. 52 0
      app/src/main/java/com/rdiot/yx485/bean/FamilyData.kt
  87. 55 0
      app/src/main/java/com/rdiot/yx485/bean/LoginData.kt
  88. 38 0
      app/src/main/java/com/rdiot/yx485/bean/MemberData.kt
  89. 82 0
      app/src/main/java/com/rdiot/yx485/bean/RoomData.kt
  90. 12 0
      app/src/main/java/com/rdiot/yx485/bean/SceneBean.kt
  91. 71 0
      app/src/main/java/com/rdiot/yx485/bean/UserData.kt
  92. 55 0
      app/src/main/java/com/rdiot/yx485/bean/WeatherData.kt
  93. 106 0
      app/src/main/java/com/rdiot/yx485/bean/YXBean.kt
  94. 445 0
      app/src/main/java/com/rdiot/yx485/net/Api.kt
  95. 32 0
      app/src/main/java/com/rdiot/yx485/net/MyRequestInterceptor.kt
  96. 80 0
      app/src/main/java/com/rdiot/yx485/net/SerializationConverter.kt
  97. 7 0
      app/src/main/java/com/rdiot/yx485/net/update/DownloadListener.kt
  98. 95 0
      app/src/main/java/com/rdiot/yx485/net/update/DownloadTask.kt
  99. 125 0
      app/src/main/java/com/rdiot/yx485/net/update/HttpUtil.kt
  100. 63 0
      app/src/main/java/com/rdiot/yx485/net/update/NotificationDownloadListener.kt

+ 18 - 0
.gitignore

@@ -0,0 +1,18 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
+.cxx
+local.properties
+/FastBleLib/build
+/*.log
+*.log

+ 3 - 0
.idea/.gitignore

@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml

+ 1 - 0
.idea/.name

@@ -0,0 +1 @@
+YX 485

+ 117 - 0
.idea/codeStyles/Project.xml

@@ -0,0 +1,117 @@
+<component name="ProjectCodeStyleConfiguration">
+  <code_scheme name="Project" version="173">
+    <codeStyleSettings language="XML">
+      <option name="FORCE_REARRANGE_MODE" value="1" />
+      <indentOptions>
+        <option name="CONTINUATION_INDENT_SIZE" value="4" />
+      </indentOptions>
+      <arrangement>
+        <rules>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>xmlns:android</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>xmlns:.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*:id</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*:name</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>name</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>style</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>^$</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>ANDROID_ATTRIBUTE_ORDER</order>
+            </rule>
+          </section>
+          <section>
+            <rule>
+              <match>
+                <AND>
+                  <NAME>.*</NAME>
+                  <XML_ATTRIBUTE />
+                  <XML_NAMESPACE>.*</XML_NAMESPACE>
+                </AND>
+              </match>
+              <order>BY_NAME</order>
+            </rule>
+          </section>
+        </rules>
+      </arrangement>
+    </codeStyleSettings>
+  </code_scheme>
+</component>

+ 5 - 0
.idea/codeStyles/codeStyleConfig.xml

@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
+  </state>
+</component>

+ 6 - 0
.idea/compiler.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="CompilerConfiguration">
+    <bytecodeTargetLevel target="11" />
+  </component>
+</project>

+ 25 - 0
.idea/deploymentTargetDropDown.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="deploymentTargetDropDown">
+    <runningDeviceTargetsSelectedWithDialog>
+      <Target>
+        <type value="RUNNING_DEVICE_TARGET" />
+        <deviceKey>
+          <Key>
+            <type value="SERIAL_NUMBER" />
+            <value value="192.168.1.186:5555" />
+          </Key>
+        </deviceKey>
+      </Target>
+      <Target>
+        <type value="RUNNING_DEVICE_TARGET" />
+        <deviceKey>
+          <Key>
+            <type value="SERIAL_NUMBER" />
+            <value value="a4426be8" />
+          </Key>
+        </deviceKey>
+      </Target>
+    </runningDeviceTargetsSelectedWithDialog>
+  </component>
+</project>

+ 24 - 0
.idea/gradle.xml

@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="GradleMigrationSettings" migrationVersion="1" />
+  <component name="GradleSettings">
+    <option name="linkedExternalProjectsSettings">
+      <GradleProjectSettings>
+        <option name="testRunner" value="GRADLE" />
+        <option name="distributionType" value="DEFAULT_WRAPPED" />
+        <option name="externalProjectPath" value="$PROJECT_DIR$" />
+        <option name="gradleJvm" value="11" />
+        <option name="modules">
+          <set>
+            <option value="$PROJECT_DIR$" />
+            <option value="$PROJECT_DIR$/FastBleLib" />
+            <option value="$PROJECT_DIR$/RangeSeekBar" />
+            <option value="$PROJECT_DIR$/app" />
+            <option value="$PROJECT_DIR$/arcseekbar" />
+            <option value="$PROJECT_DIR$/pageview" />
+          </set>
+        </option>
+      </GradleProjectSettings>
+    </option>
+  </component>
+</project>

+ 6 - 0
.idea/kotlinc.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="KotlinJpsPluginSettings">
+    <option name="version" value="1.7.10" />
+  </component>
+</project>

+ 83 - 0
.idea/misc.xml

@@ -0,0 +1,83 @@
+<project version="4">
+  <component name="DesignSurface">
+    <option name="filePathToZoomLevelMap">
+      <map>
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bar_constact.xml" value="0.2365" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bar_news.xml" value="0.241" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_badge.xml" value="0.2225" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_device_ctrl_btn.xml" value="0.2295" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_device_ctrl_btn_pressed.xml" value="0.2295" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_home_no_room_add_btn.xml" value="0.2655" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_home_scene.xml" value="0.2695" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_home_weather.xml" value="0.139" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_login_btn.xml" value="0.2325" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_login_btn_pressed.xml" value="0.2325" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_login_get_sms_btn.xml" value="0.2325" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_login_get_sms_btn_disable.xml" value="0.2555" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_login_get_sms_btn_pressed.xml" value="0.2555" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_mine_option.xml" value="0.2485" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_my_home_banner.xml" value="0.11" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_popup_main_add.xml" value="0.273" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_room_device_off.xml" value="0.2305" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_room_device_on.xml" value="0.254" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_room_item.xml" value="0.2305" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/bg_tab_text_color.xml" value="0.176" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/item_check.xml" value="0.2245" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/sel_check_box.xml" value="0.2295" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/sel_device_ctrl_btn.xml" value="0.2295" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/sel_login_btn.xml" value="0.2315" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/sel_login_get_sms_btn.xml" value="0.2545" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/sel_login_get_sms_btn_text.xml" value="0.2545" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/sel_room_ctrl_btn.xml" value="0.2465" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/sel_tab_find.xml" value="0.2225" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/sel_tab_home.xml" value="0.2225" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/sel_tab_mine.xml" value="0.2225" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/sel_tab_room.xml" value="0.2225" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/sel_tab_text_color.xml" value="0.2555" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/drawable/sel_warm_mode.xml" value="0.2295" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/act_bind.xml" value="0.18802083333333333" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/act_family_list.xml" value="0.22103463018383926" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/act_family_management.xml" value="0.25" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/act_login.xml" value="0.22604166666666667" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/act_main.xml" value="0.3504854368932039" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/act_main2.xml" value="0.2677083333333333" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/act_qr_code.xml" value="0.21" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/act_room_ctrl.xml" value="0.43359375" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/activity_main.xml" value="0.22" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_bind_device.xml" value="0.2984375" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_device_ctrl.xml" value="0.22291666666666668" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_enter_room_info.xml" value="0.27254374158815614" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_enter_wifi_info.xml" value="0.4106280193236715" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_find.xml" value="0.25" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_forget_pwd.xml" value="0.2890625" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_home.xml" value="0.38155668358714045" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_login_phone.xml" value="0.3645833333333333" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_login_pwd.xml" value="0.25" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_login_sms.xml" value="0.25" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_main.xml" value="0.26875" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_mine.xml" value="0.3391243919388464" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_reset.xml" value="0.18802083333333333" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_reset_pwd.xml" value="0.3382084095063985" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_room.xml" value="0.25" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_room_ctrl.xml" value="0.25729166666666664" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/fra_room_info_input.xml" value="0.27395833333333336" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/item_bottom_tab.xml" value="0.75" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/item_family.xml" value="0.33" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/item_horizontal_grid.xml" value="0.365625" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/item_room.xml" value="0.25" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/item_scene.xml" value="0.33" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/popup_main_add.xml" value="0.35104166666666664" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/layout/view_navbar.xml" value="0.33" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/app/src/main/res/menu/navigation_menu.xml" value="0.23020833333333332" />
+        <entry key="..\:/Work/AndroidPorjects/RD/YongXu485/pageview/src/main/res/drawable/shape_point_unselected.xml" value="0.1935" />
+        <entry key="..\:/Work/Cache/.gradle/caches/transforms-3/b75bde5c00aa87c2e7752c5e8569bc51/transformed/zxing-lite-2.2.1/res/layout/zxl_capture.xml" value="0.1731509191962377" />
+      </map>
+    </option>
+  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
+    <output url="file://$PROJECT_DIR$/build/classes" />
+  </component>
+  <component name="ProjectType">
+    <option name="id" value="Android" />
+  </component>
+</project>

+ 6 - 0
.idea/render.experimental.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="RenderSettings">
+    <option name="quality" value="0.0" />
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="$PROJECT_DIR$" vcs="Git" />
+  </component>
+</project>

+ 31 - 0
FastBleLib/build.gradle

@@ -0,0 +1,31 @@
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 30
+    buildToolsVersion "30.0.3"
+
+    defaultConfig {
+        minSdkVersion 14
+        targetSdkVersion 30
+        versionCode 240
+        versionName "2.4.0"
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+}
+
+task makeAAR(type: Copy) {
+    from('build/outputs/aar/')
+    into('build/aarFloder/')
+    include('FastBleLib-release.aar')
+    rename ('FastBleLib-release.aar', 'FastBLE-2.4.0.aar' )
+}
+makeAAR.dependsOn(build)

+ 17 - 0
FastBleLib/proguard-rules.pro

@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in D:\development\AppData\sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}

+ 42 - 0
FastBleLib/src/main/AndroidManifest.xml

@@ -0,0 +1,42 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.clj.fastble">
+
+
+    <!-- Android 12以下配置的三个蓝牙权限 -->
+    <!-- 这个权限允许程序连接到已配对的蓝牙设备, 请求连接/接收连接/传输数据需要改权限, 主要用于对配对后进行操作 -->
+    <uses-permission
+        android:name="android.permission.BLUETOOTH"
+        android:maxSdkVersion="30" />
+
+    <!-- 这个权限允许程序发现和配对蓝牙设备, 该权限用来管理蓝牙设备, 有了这个权限, 应用才能使用本机的蓝牙设备, 主要用于对配对前的操作 -->
+    <uses-permission
+        android:name="android.permission.BLUETOOTH_ADMIN"
+        android:maxSdkVersion="30" />
+
+    <!-- Android 6.0以后,12.0以下,这两个权限是必须的,蓝牙扫描周围的设备需要获取模糊的位置信息。
+   这两个权限属于同一组危险权限,在清单文件中声明之后,还需要再运行时动态获取。 -->
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <!-- Android 12以下配置的两个蓝牙权限 -->
+
+
+
+
+    <!-- Android 12 及以上版本配置的三个权限 -->
+    <!-- 您的应用查找蓝牙设备(如蓝牙低功耗 (BLE) 外围设备)-->
+    <uses-permission
+        android:name="android.permission.BLUETOOTH_SCAN"
+        android:usesPermissionFlags="neverForLocation"
+        tools:targetApi="s" />
+
+    <!-- 应用程序使手机蓝牙可被其它蓝牙设备发现时才需要-->
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+
+    <!-- 仅应用程序与已配对的蓝牙设备通信时才需要 -->
+    <uses-permission
+        android:name="android.permission.BLUETOOTH_CONNECT"
+        tools:targetApi="s" />
+    <!-- Android 12 及以上版本配置的三个权限 -->
+
+</manifest>

+ 936 - 0
FastBleLib/src/main/java/com/clj/fastble/BleManager.java

@@ -0,0 +1,936 @@
+package com.clj.fastble;
+
+import android.annotation.TargetApi;
+import android.app.Application;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattService;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.le.ScanRecord;
+import android.bluetooth.le.ScanResult;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Looper;
+
+import com.clj.fastble.bluetooth.BleBluetooth;
+import com.clj.fastble.bluetooth.MultipleBluetoothController;
+import com.clj.fastble.bluetooth.SplitWriter;
+import com.clj.fastble.callback.BleGattCallback;
+import com.clj.fastble.callback.BleIndicateCallback;
+import com.clj.fastble.callback.BleMtuChangedCallback;
+import com.clj.fastble.callback.BleNotifyCallback;
+import com.clj.fastble.callback.BleReadCallback;
+import com.clj.fastble.callback.BleRssiCallback;
+import com.clj.fastble.callback.BleScanAndConnectCallback;
+import com.clj.fastble.callback.BleScanCallback;
+import com.clj.fastble.callback.BleWriteCallback;
+import com.clj.fastble.data.BleDevice;
+import com.clj.fastble.data.BleScanState;
+import com.clj.fastble.exception.OtherException;
+import com.clj.fastble.scan.BleScanRuleConfig;
+import com.clj.fastble.scan.BleScanner;
+import com.clj.fastble.utils.BleLog;
+
+import java.util.List;
+import java.util.UUID;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public class BleManager {
+
+    private Application context;
+    private BleScanRuleConfig bleScanRuleConfig;
+    private BluetoothAdapter bluetoothAdapter;
+    private MultipleBluetoothController multipleBluetoothController;
+    private BluetoothManager bluetoothManager;
+
+    public static final int DEFAULT_SCAN_TIME = 10000;
+    private static final int DEFAULT_MAX_MULTIPLE_DEVICE = 7;
+    private static final int DEFAULT_OPERATE_TIME = 5000;
+    private static final int DEFAULT_CONNECT_RETRY_COUNT = 0;
+    private static final int DEFAULT_CONNECT_RETRY_INTERVAL = 5000;
+    private static final int DEFAULT_MTU = 23;
+    private static final int DEFAULT_MAX_MTU = 512;
+    private static final int DEFAULT_WRITE_DATA_SPLIT_COUNT = 20;
+    private static final int DEFAULT_CONNECT_OVER_TIME = 10000;
+
+    private int maxConnectCount = DEFAULT_MAX_MULTIPLE_DEVICE;
+    private int operateTimeout = DEFAULT_OPERATE_TIME;
+    private int reConnectCount = DEFAULT_CONNECT_RETRY_COUNT;
+    private long reConnectInterval = DEFAULT_CONNECT_RETRY_INTERVAL;
+    private int splitWriteNum = DEFAULT_WRITE_DATA_SPLIT_COUNT;
+    private long connectOverTime = DEFAULT_CONNECT_OVER_TIME;
+
+    public static BleManager getInstance() {
+        return BleManagerHolder.sBleManager;
+    }
+
+    private static class BleManagerHolder {
+        private static final BleManager sBleManager = new BleManager();
+    }
+
+    public void init(Application app) {
+        if (context == null && app != null) {
+            context = app;
+            if (isSupportBle()) {
+                bluetoothManager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
+            }
+            bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+            multipleBluetoothController = new MultipleBluetoothController();
+            bleScanRuleConfig = new BleScanRuleConfig();
+        }
+    }
+
+    /**
+     * Get the Context
+     *
+     * @return
+     */
+    public Context getContext() {
+        return context;
+    }
+
+    /**
+     * Get the BluetoothManager
+     *
+     * @return
+     */
+    public BluetoothManager getBluetoothManager() {
+        return bluetoothManager;
+    }
+
+    /**
+     * Get the BluetoothAdapter
+     *
+     * @return
+     */
+    public BluetoothAdapter getBluetoothAdapter() {
+        return bluetoothAdapter;
+    }
+
+    /**
+     * get the ScanRuleConfig
+     *
+     * @return
+     */
+    public BleScanRuleConfig getScanRuleConfig() {
+        return bleScanRuleConfig;
+    }
+
+    /**
+     * Get the multiple Bluetooth Controller
+     *
+     * @return
+     */
+    public MultipleBluetoothController getMultipleBluetoothController() {
+        return multipleBluetoothController;
+    }
+
+    /**
+     * Configure scan and connection properties
+     *
+     * @param config
+     */
+    public void initScanRule(BleScanRuleConfig config) {
+        this.bleScanRuleConfig = config;
+    }
+
+    /**
+     * Get the maximum number of connections
+     *
+     * @return
+     */
+    public int getMaxConnectCount() {
+        return maxConnectCount;
+    }
+
+    /**
+     * Set the maximum number of connections
+     *
+     * @param count
+     * @return BleManager
+     */
+    public BleManager setMaxConnectCount(int count) {
+        if (count > DEFAULT_MAX_MULTIPLE_DEVICE)
+            count = DEFAULT_MAX_MULTIPLE_DEVICE;
+        this.maxConnectCount = count;
+        return this;
+    }
+
+    /**
+     * Get operate timeout
+     *
+     * @return
+     */
+    public int getOperateTimeout() {
+        return operateTimeout;
+    }
+
+    /**
+     * Set operate timeout
+     *
+     * @param count
+     * @return BleManager
+     */
+    public BleManager setOperateTimeout(int count) {
+        this.operateTimeout = count;
+        return this;
+    }
+
+    /**
+     * Get connect retry count
+     *
+     * @return
+     */
+    public int getReConnectCount() {
+        return reConnectCount;
+    }
+
+    /**
+     * Get connect retry interval
+     *
+     * @return
+     */
+    public long getReConnectInterval() {
+        return reConnectInterval;
+    }
+
+    /**
+     * Set connect retry count and interval
+     *
+     * @param count
+     * @return BleManager
+     */
+    public BleManager setReConnectCount(int count) {
+        return setReConnectCount(count, DEFAULT_CONNECT_RETRY_INTERVAL);
+    }
+
+    /**
+     * Set connect retry count and interval
+     *
+     * @param count
+     * @return BleManager
+     */
+    public BleManager setReConnectCount(int count, long interval) {
+        if (count > 10)
+            count = 10;
+        if (interval < 0)
+            interval = 0;
+        this.reConnectCount = count;
+        this.reConnectInterval = interval;
+        return this;
+    }
+
+
+    /**
+     * Get operate split Write Num
+     *
+     * @return
+     */
+    public int getSplitWriteNum() {
+        return splitWriteNum;
+    }
+
+    /**
+     * Set split Writ eNum
+     *
+     * @param num
+     * @return BleManager
+     */
+    public BleManager setSplitWriteNum(int num) {
+        if (num > 0) {
+            this.splitWriteNum = num;
+        }
+        return this;
+    }
+
+    /**
+     * Get operate connect Over Time
+     *
+     * @return
+     */
+    public long getConnectOverTime() {
+        return connectOverTime;
+    }
+
+    /**
+     * Set connect Over Time
+     *
+     * @param time
+     * @return BleManager
+     */
+    public BleManager setConnectOverTime(long time) {
+        if (time <= 0) {
+            time = 100;
+        }
+        this.connectOverTime = time;
+        return this;
+    }
+
+    /**
+     * print log?
+     *
+     * @param enable
+     * @return BleManager
+     */
+    public BleManager enableLog(boolean enable) {
+        BleLog.isPrint = enable;
+        return this;
+    }
+
+    /**
+     * scan device around
+     *
+     * @param callback
+     */
+    public void scan(BleScanCallback callback) {
+        if (callback == null) {
+            throw new IllegalArgumentException("BleScanCallback can not be Null!");
+        }
+
+        if (!isBlueEnable()) {
+            BleLog.e("Bluetooth not enable!");
+            callback.onScanStarted(false);
+            return;
+        }
+
+        UUID[] serviceUuids = bleScanRuleConfig.getServiceUuids();
+        String[] deviceNames = bleScanRuleConfig.getDeviceNames();
+        String deviceMac = bleScanRuleConfig.getDeviceMac();
+        boolean fuzzy = bleScanRuleConfig.isFuzzy();
+        long timeOut = bleScanRuleConfig.getScanTimeOut();
+
+        BleScanner.getInstance().scan(serviceUuids, deviceNames, deviceMac, fuzzy, timeOut, callback, bleScanRuleConfig.isCanRepeatFound());
+    }
+
+    /**
+     * scan device then connect
+     *
+     * @param callback
+     */
+    public void scanAndConnect(BleScanAndConnectCallback callback) {
+        if (callback == null) {
+            throw new IllegalArgumentException("BleScanAndConnectCallback can not be Null!");
+        }
+
+        if (!isBlueEnable()) {
+            BleLog.e("Bluetooth not enable!");
+            callback.onScanStarted(false);
+            return;
+        }
+
+        UUID[] serviceUuids = bleScanRuleConfig.getServiceUuids();
+        String[] deviceNames = bleScanRuleConfig.getDeviceNames();
+        String deviceMac = bleScanRuleConfig.getDeviceMac();
+        boolean fuzzy = bleScanRuleConfig.isFuzzy();
+        long timeOut = bleScanRuleConfig.getScanTimeOut();
+
+        BleScanner.getInstance().scanAndConnect(serviceUuids, deviceNames, deviceMac, fuzzy, timeOut, callback);
+    }
+
+    /**
+     * connect a known device
+     *
+     * @param bleDevice
+     * @param bleGattCallback
+     * @return
+     */
+    public BluetoothGatt connect(BleDevice bleDevice, BleGattCallback bleGattCallback) {
+        if (bleGattCallback == null) {
+            throw new IllegalArgumentException("BleGattCallback can not be Null!");
+        }
+
+        if (!isBlueEnable()) {
+            BleLog.e("Bluetooth not enable!");
+            bleGattCallback.onConnectFail(bleDevice, new OtherException("Bluetooth not enable!"));
+            return null;
+        }
+
+        if (Looper.myLooper() == null || Looper.myLooper() != Looper.getMainLooper()) {
+            BleLog.w("Be careful: currentThread is not MainThread!");
+        }
+
+        if (bleDevice == null || bleDevice.getDevice() == null) {
+            bleGattCallback.onConnectFail(bleDevice, new OtherException("Not Found Device Exception Occurred!"));
+        } else {
+            BleBluetooth bleBluetooth = multipleBluetoothController.buildConnectingBle(bleDevice);
+            boolean autoConnect = bleScanRuleConfig.isAutoConnect();
+            return bleBluetooth.connect(bleDevice, autoConnect, bleGattCallback);
+        }
+
+        return null;
+    }
+
+    /**
+     * connect a device through its mac without scan,whether or not it has been connected
+     *
+     * @param mac
+     * @param bleGattCallback
+     * @return
+     */
+    public BluetoothGatt connect(String mac, BleGattCallback bleGattCallback) {
+        BluetoothDevice bluetoothDevice = getBluetoothAdapter().getRemoteDevice(mac);
+        BleDevice bleDevice = new BleDevice(bluetoothDevice, 0, null, 0);
+        return connect(bleDevice, bleGattCallback);
+    }
+
+
+    /**
+     * Cancel scan
+     */
+    public void cancelScan() {
+        BleScanner.getInstance().stopLeScan();
+    }
+
+    /**
+     * notify
+     *
+     * @param bleDevice
+     * @param uuid_service
+     * @param uuid_notify
+     * @param callback
+     */
+    public void notify(BleDevice bleDevice,
+                       String uuid_service,
+                       String uuid_notify,
+                       BleNotifyCallback callback) {
+        notify(bleDevice, uuid_service, uuid_notify, false, callback);
+    }
+
+    /**
+     * notify
+     *
+     * @param bleDevice
+     * @param uuid_service
+     * @param uuid_notify
+     * @param useCharacteristicDescriptor
+     * @param callback
+     */
+    public void notify(BleDevice bleDevice,
+                       String uuid_service,
+                       String uuid_notify,
+                       boolean useCharacteristicDescriptor,
+                       BleNotifyCallback callback) {
+        if (callback == null) {
+            throw new IllegalArgumentException("BleNotifyCallback can not be Null!");
+        }
+
+        BleBluetooth bleBluetooth = multipleBluetoothController.getBleBluetooth(bleDevice);
+        if (bleBluetooth == null) {
+            callback.onNotifyFailure(new OtherException("This device not connect!"));
+        } else {
+            bleBluetooth.newBleConnector()
+                    .withUUIDString(uuid_service, uuid_notify)
+                    .enableCharacteristicNotify(callback, uuid_notify, useCharacteristicDescriptor);
+        }
+    }
+
+    /**
+     * indicate
+     *
+     * @param bleDevice
+     * @param uuid_service
+     * @param uuid_indicate
+     * @param callback
+     */
+    public void indicate(BleDevice bleDevice,
+                         String uuid_service,
+                         String uuid_indicate,
+                         BleIndicateCallback callback) {
+        indicate(bleDevice, uuid_service, uuid_indicate, false, callback);
+    }
+
+    /**
+     * indicate
+     *
+     * @param bleDevice
+     * @param uuid_service
+     * @param uuid_indicate
+     * @param useCharacteristicDescriptor
+     * @param callback
+     */
+    public void indicate(BleDevice bleDevice,
+                         String uuid_service,
+                         String uuid_indicate,
+                         boolean useCharacteristicDescriptor,
+                         BleIndicateCallback callback) {
+        if (callback == null) {
+            throw new IllegalArgumentException("BleIndicateCallback can not be Null!");
+        }
+
+        BleBluetooth bleBluetooth = multipleBluetoothController.getBleBluetooth(bleDevice);
+        if (bleBluetooth == null) {
+            callback.onIndicateFailure(new OtherException("This device not connect!"));
+        } else {
+            bleBluetooth.newBleConnector()
+                    .withUUIDString(uuid_service, uuid_indicate)
+                    .enableCharacteristicIndicate(callback, uuid_indicate, useCharacteristicDescriptor);
+        }
+    }
+
+    /**
+     * stop notify, remove callback
+     *
+     * @param bleDevice
+     * @param uuid_service
+     * @param uuid_notify
+     * @return
+     */
+    public boolean stopNotify(BleDevice bleDevice,
+                              String uuid_service,
+                              String uuid_notify) {
+        return stopNotify(bleDevice, uuid_service, uuid_notify, false);
+    }
+
+    /**
+     * stop notify, remove callback
+     *
+     * @param bleDevice
+     * @param uuid_service
+     * @param uuid_notify
+     * @param useCharacteristicDescriptor
+     * @return
+     */
+    public boolean stopNotify(BleDevice bleDevice,
+                              String uuid_service,
+                              String uuid_notify,
+                              boolean useCharacteristicDescriptor) {
+        BleBluetooth bleBluetooth = multipleBluetoothController.getBleBluetooth(bleDevice);
+        if (bleBluetooth == null) {
+            return false;
+        }
+        boolean success = bleBluetooth.newBleConnector()
+                .withUUIDString(uuid_service, uuid_notify)
+                .disableCharacteristicNotify(useCharacteristicDescriptor);
+        if (success) {
+            bleBluetooth.removeNotifyCallback(uuid_notify);
+        }
+        return success;
+    }
+
+    /**
+     * stop indicate, remove callback
+     *
+     * @param bleDevice
+     * @param uuid_service
+     * @param uuid_indicate
+     * @return
+     */
+    public boolean stopIndicate(BleDevice bleDevice,
+                                String uuid_service,
+                                String uuid_indicate) {
+        return stopIndicate(bleDevice, uuid_service, uuid_indicate, false);
+    }
+
+    /**
+     * stop indicate, remove callback
+     *
+     * @param bleDevice
+     * @param uuid_service
+     * @param uuid_indicate
+     * @param useCharacteristicDescriptor
+     * @return
+     */
+    public boolean stopIndicate(BleDevice bleDevice,
+                                String uuid_service,
+                                String uuid_indicate,
+                                boolean useCharacteristicDescriptor) {
+        BleBluetooth bleBluetooth = multipleBluetoothController.getBleBluetooth(bleDevice);
+        if (bleBluetooth == null) {
+            return false;
+        }
+        boolean success = bleBluetooth.newBleConnector()
+                .withUUIDString(uuid_service, uuid_indicate)
+                .disableCharacteristicIndicate(useCharacteristicDescriptor);
+        if (success) {
+            bleBluetooth.removeIndicateCallback(uuid_indicate);
+        }
+        return success;
+    }
+
+    /**
+     * write
+     *
+     * @param bleDevice
+     * @param uuid_service
+     * @param uuid_write
+     * @param data
+     * @param callback
+     */
+    public void write(BleDevice bleDevice,
+                      String uuid_service,
+                      String uuid_write,
+                      byte[] data,
+                      BleWriteCallback callback) {
+        write(bleDevice, uuid_service, uuid_write, data, true, callback);
+    }
+
+    /**
+     * write
+     *
+     * @param bleDevice
+     * @param uuid_service
+     * @param uuid_write
+     * @param data
+     * @param split
+     * @param callback
+     */
+    public void write(BleDevice bleDevice,
+                      String uuid_service,
+                      String uuid_write,
+                      byte[] data,
+                      boolean split,
+                      BleWriteCallback callback) {
+
+        write(bleDevice, uuid_service, uuid_write, data, split, true, 0, callback);
+    }
+
+    /**
+     * write
+     *
+     * @param bleDevice
+     * @param uuid_service
+     * @param uuid_write
+     * @param data
+     * @param split
+     * @param sendNextWhenLastSuccess
+     * @param intervalBetweenTwoPackage
+     * @param callback
+     */
+    public void write(BleDevice bleDevice,
+                      String uuid_service,
+                      String uuid_write,
+                      byte[] data,
+                      boolean split,
+                      boolean sendNextWhenLastSuccess,
+                      long intervalBetweenTwoPackage,
+                      BleWriteCallback callback) {
+
+        if (callback == null) {
+            throw new IllegalArgumentException("BleWriteCallback can not be Null!");
+        }
+
+        if (data == null) {
+            BleLog.e("data is Null!");
+            callback.onWriteFailure(new OtherException("data is Null!"));
+            return;
+        }
+
+        if (data.length > 20 && !split) {
+            BleLog.w("Be careful: data's length beyond 20! Ensure MTU higher than 23, or use spilt write!");
+        }
+
+        BleBluetooth bleBluetooth = multipleBluetoothController.getBleBluetooth(bleDevice);
+        if (bleBluetooth == null) {
+            callback.onWriteFailure(new OtherException("This device not connect!"));
+        } else {
+            if (split && data.length > getSplitWriteNum()) {
+                new SplitWriter().splitWrite(bleBluetooth, uuid_service, uuid_write, data,
+                        sendNextWhenLastSuccess, intervalBetweenTwoPackage, callback);
+            } else {
+                bleBluetooth.newBleConnector()
+                        .withUUIDString(uuid_service, uuid_write)
+                        .writeCharacteristic(data, callback, uuid_write);
+            }
+        }
+    }
+
+    /**
+     * read
+     *
+     * @param bleDevice
+     * @param uuid_service
+     * @param uuid_read
+     * @param callback
+     */
+    public void read(BleDevice bleDevice,
+                     String uuid_service,
+                     String uuid_read,
+                     BleReadCallback callback) {
+        if (callback == null) {
+            throw new IllegalArgumentException("BleReadCallback can not be Null!");
+        }
+
+        BleBluetooth bleBluetooth = multipleBluetoothController.getBleBluetooth(bleDevice);
+        if (bleBluetooth == null) {
+            callback.onReadFailure(new OtherException("This device is not connected!"));
+        } else {
+            bleBluetooth.newBleConnector()
+                    .withUUIDString(uuid_service, uuid_read)
+                    .readCharacteristic(callback, uuid_read);
+        }
+    }
+
+    /**
+     * read Rssi
+     *
+     * @param bleDevice
+     * @param callback
+     */
+    public void readRssi(BleDevice bleDevice,
+                         BleRssiCallback callback) {
+        if (callback == null) {
+            throw new IllegalArgumentException("BleRssiCallback can not be Null!");
+        }
+
+        BleBluetooth bleBluetooth = multipleBluetoothController.getBleBluetooth(bleDevice);
+        if (bleBluetooth == null) {
+            callback.onRssiFailure(new OtherException("This device is not connected!"));
+        } else {
+            bleBluetooth.newBleConnector().readRemoteRssi(callback);
+        }
+    }
+
+    /**
+     * set Mtu
+     *
+     * @param bleDevice
+     * @param mtu
+     * @param callback
+     */
+    public void setMtu(BleDevice bleDevice,
+                       int mtu,
+                       BleMtuChangedCallback callback) {
+        if (callback == null) {
+            throw new IllegalArgumentException("BleMtuChangedCallback can not be Null!");
+        }
+
+        if (mtu > DEFAULT_MAX_MTU) {
+            BleLog.e("requiredMtu should lower than 512 !");
+            callback.onSetMTUFailure(new OtherException("requiredMtu should lower than 512 !"));
+            return;
+        }
+
+        if (mtu < DEFAULT_MTU) {
+            BleLog.e("requiredMtu should higher than 23 !");
+            callback.onSetMTUFailure(new OtherException("requiredMtu should higher than 23 !"));
+            return;
+        }
+
+        BleBluetooth bleBluetooth = multipleBluetoothController.getBleBluetooth(bleDevice);
+        if (bleBluetooth == null) {
+            callback.onSetMTUFailure(new OtherException("This device is not connected!"));
+        } else {
+            bleBluetooth.newBleConnector().setMtu(mtu, callback);
+        }
+    }
+
+    /**
+     * requestConnectionPriority
+     *
+     * @param connectionPriority Request a specific connection priority. Must be one of
+     *                           {@link BluetoothGatt#CONNECTION_PRIORITY_BALANCED},
+     *                           {@link BluetoothGatt#CONNECTION_PRIORITY_HIGH}
+     *                           or {@link BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER}.
+     * @throws IllegalArgumentException If the parameters are outside of their
+     *                                  specified range.
+     */
+    public boolean requestConnectionPriority(BleDevice bleDevice, int connectionPriority) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            BleBluetooth bleBluetooth = multipleBluetoothController.getBleBluetooth(bleDevice);
+            if (bleBluetooth == null) {
+                return false;
+            } else {
+                return bleBluetooth.newBleConnector().requestConnectionPriority(connectionPriority);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * is support ble?
+     *
+     * @return
+     */
+    public boolean isSupportBle() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2
+                && context.getApplicationContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
+    }
+
+    /**
+     * Open bluetooth
+     */
+    public void enableBluetooth() {
+        if (bluetoothAdapter != null) {
+            bluetoothAdapter.enable();
+        }
+    }
+
+    /**
+     * Disable bluetooth
+     */
+    public void disableBluetooth() {
+        if (bluetoothAdapter != null) {
+            if (bluetoothAdapter.isEnabled())
+                bluetoothAdapter.disable();
+        }
+    }
+
+    /**
+     * judge Bluetooth is enable
+     *
+     * @return
+     */
+    public boolean isBlueEnable() {
+        return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
+    }
+
+
+    public BleDevice convertBleDevice(BluetoothDevice bluetoothDevice) {
+        return new BleDevice(bluetoothDevice);
+    }
+
+    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
+    public BleDevice convertBleDevice(ScanResult scanResult) {
+        if (scanResult == null) {
+            throw new IllegalArgumentException("scanResult can not be Null!");
+        }
+        BluetoothDevice bluetoothDevice = scanResult.getDevice();
+        int rssi = scanResult.getRssi();
+        ScanRecord scanRecord = scanResult.getScanRecord();
+        byte[] bytes = null;
+        if (scanRecord != null)
+            bytes = scanRecord.getBytes();
+        long timestampNanos = scanResult.getTimestampNanos();
+        return new BleDevice(bluetoothDevice, rssi, bytes, timestampNanos);
+    }
+
+    public BleBluetooth getBleBluetooth(BleDevice bleDevice) {
+        if (multipleBluetoothController != null) {
+            return multipleBluetoothController.getBleBluetooth(bleDevice);
+        }
+        return null;
+    }
+
+    public BluetoothGatt getBluetoothGatt(BleDevice bleDevice) {
+        BleBluetooth bleBluetooth = getBleBluetooth(bleDevice);
+        if (bleBluetooth != null)
+            return bleBluetooth.getBluetoothGatt();
+        return null;
+    }
+
+    public List<BluetoothGattService> getBluetoothGattServices(BleDevice bleDevice) {
+        BluetoothGatt gatt = getBluetoothGatt(bleDevice);
+        if (gatt != null) {
+            return gatt.getServices();
+        }
+        return null;
+    }
+
+    public List<BluetoothGattCharacteristic> getBluetoothGattCharacteristics(BluetoothGattService service) {
+        return service.getCharacteristics();
+    }
+
+    public void removeConnectGattCallback(BleDevice bleDevice) {
+        BleBluetooth bleBluetooth = getBleBluetooth(bleDevice);
+        if (bleBluetooth != null)
+            bleBluetooth.removeConnectGattCallback();
+    }
+
+    public void removeRssiCallback(BleDevice bleDevice) {
+        BleBluetooth bleBluetooth = getBleBluetooth(bleDevice);
+        if (bleBluetooth != null)
+            bleBluetooth.removeRssiCallback();
+    }
+
+    public void removeMtuChangedCallback(BleDevice bleDevice) {
+        BleBluetooth bleBluetooth = getBleBluetooth(bleDevice);
+        if (bleBluetooth != null)
+            bleBluetooth.removeMtuChangedCallback();
+    }
+
+    public void removeNotifyCallback(BleDevice bleDevice, String uuid_notify) {
+        BleBluetooth bleBluetooth = getBleBluetooth(bleDevice);
+        if (bleBluetooth != null)
+            bleBluetooth.removeNotifyCallback(uuid_notify);
+    }
+
+    public void removeIndicateCallback(BleDevice bleDevice, String uuid_indicate) {
+        BleBluetooth bleBluetooth = getBleBluetooth(bleDevice);
+        if (bleBluetooth != null)
+            bleBluetooth.removeIndicateCallback(uuid_indicate);
+    }
+
+    public void removeWriteCallback(BleDevice bleDevice, String uuid_write) {
+        BleBluetooth bleBluetooth = getBleBluetooth(bleDevice);
+        if (bleBluetooth != null)
+            bleBluetooth.removeWriteCallback(uuid_write);
+    }
+
+    public void removeReadCallback(BleDevice bleDevice, String uuid_read) {
+        BleBluetooth bleBluetooth = getBleBluetooth(bleDevice);
+        if (bleBluetooth != null)
+            bleBluetooth.removeReadCallback(uuid_read);
+    }
+
+    public void clearCharacterCallback(BleDevice bleDevice) {
+        BleBluetooth bleBluetooth = getBleBluetooth(bleDevice);
+        if (bleBluetooth != null)
+            bleBluetooth.clearCharacterCallback();
+    }
+
+    public BleScanState getScanSate() {
+        return BleScanner.getInstance().getScanState();
+    }
+
+    public List<BleDevice> getAllConnectedDevice() {
+        if (multipleBluetoothController == null)
+            return null;
+        return multipleBluetoothController.getDeviceList();
+    }
+
+    /**
+     * @param bleDevice
+     * @return State of the profile connection. One of
+     * {@link BluetoothProfile#STATE_CONNECTED},
+     * {@link BluetoothProfile#STATE_CONNECTING},
+     * {@link BluetoothProfile#STATE_DISCONNECTED},
+     * {@link BluetoothProfile#STATE_DISCONNECTING}
+     */
+    public int getConnectState(BleDevice bleDevice) {
+        if (bleDevice != null) {
+            return bluetoothManager.getConnectionState(bleDevice.getDevice(), BluetoothProfile.GATT);
+        } else {
+            return BluetoothProfile.STATE_DISCONNECTED;
+        }
+    }
+
+    public boolean isConnected(BleDevice bleDevice) {
+        return getConnectState(bleDevice) == BluetoothProfile.STATE_CONNECTED;
+    }
+
+    public boolean isConnected(String mac) {
+        List<BleDevice> list = getAllConnectedDevice();
+        for (BleDevice bleDevice : list) {
+            if (bleDevice != null) {
+                if (bleDevice.getMac().equals(mac)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    public void disconnect(BleDevice bleDevice) {
+        if (multipleBluetoothController != null) {
+            multipleBluetoothController.disconnect(bleDevice);
+        }
+    }
+
+    public void disconnectAllDevice() {
+        if (multipleBluetoothController != null) {
+            multipleBluetoothController.disconnectAllDevice();
+        }
+    }
+
+    public void destroy() {
+        if (multipleBluetoothController != null) {
+            multipleBluetoothController.destroy();
+        }
+    }
+
+
+}

+ 612 - 0
FastBleLib/src/main/java/com/clj/fastble/bluetooth/BleBluetooth.java

@@ -0,0 +1,612 @@
+package com.clj.fastble.bluetooth;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothProfile;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import com.clj.fastble.BleManager;
+import com.clj.fastble.callback.BleGattCallback;
+import com.clj.fastble.callback.BleIndicateCallback;
+import com.clj.fastble.callback.BleMtuChangedCallback;
+import com.clj.fastble.callback.BleNotifyCallback;
+import com.clj.fastble.callback.BleReadCallback;
+import com.clj.fastble.callback.BleRssiCallback;
+import com.clj.fastble.callback.BleWriteCallback;
+import com.clj.fastble.data.BleConnectStateParameter;
+import com.clj.fastble.data.BleDevice;
+import com.clj.fastble.data.BleMsg;
+import com.clj.fastble.exception.ConnectException;
+import com.clj.fastble.exception.OtherException;
+import com.clj.fastble.exception.TimeoutException;
+import com.clj.fastble.utils.BleLog;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import static android.bluetooth.BluetoothDevice.TRANSPORT_LE;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public class BleBluetooth {
+
+    private BleGattCallback bleGattCallback;
+    private BleRssiCallback bleRssiCallback;
+    private BleMtuChangedCallback bleMtuChangedCallback;
+    private final HashMap<String, BleNotifyCallback> bleNotifyCallbackHashMap = new HashMap<>();
+    private final HashMap<String, BleIndicateCallback> bleIndicateCallbackHashMap = new HashMap<>();
+    private final HashMap<String, BleWriteCallback> bleWriteCallbackHashMap = new HashMap<>();
+    private final HashMap<String, BleReadCallback> bleReadCallbackHashMap = new HashMap<>();
+
+    private LastState lastState;
+    private boolean isActiveDisconnect = false;
+    private final BleDevice bleDevice;
+    private BluetoothGatt bluetoothGatt;
+    private final MainHandler mainHandler = new MainHandler(Looper.getMainLooper());
+    private int connectRetryCount = 0;
+
+    public BleBluetooth(BleDevice bleDevice) {
+        this.bleDevice = bleDevice;
+    }
+
+    public BleConnector newBleConnector() {
+        return new BleConnector(this);
+    }
+
+    public synchronized void addConnectGattCallback(BleGattCallback callback) {
+        bleGattCallback = callback;
+    }
+
+    public synchronized void removeConnectGattCallback() {
+        bleGattCallback = null;
+    }
+
+    public synchronized void addNotifyCallback(String uuid, BleNotifyCallback bleNotifyCallback) {
+        bleNotifyCallbackHashMap.put(uuid, bleNotifyCallback);
+    }
+
+    public synchronized void addIndicateCallback(String uuid, BleIndicateCallback bleIndicateCallback) {
+        bleIndicateCallbackHashMap.put(uuid, bleIndicateCallback);
+    }
+
+    public synchronized void addWriteCallback(String uuid, BleWriteCallback bleWriteCallback) {
+        bleWriteCallbackHashMap.put(uuid, bleWriteCallback);
+    }
+
+    public synchronized void addReadCallback(String uuid, BleReadCallback bleReadCallback) {
+        bleReadCallbackHashMap.put(uuid, bleReadCallback);
+    }
+
+    public synchronized void removeNotifyCallback(String uuid) {
+        if (bleNotifyCallbackHashMap.containsKey(uuid))
+            bleNotifyCallbackHashMap.remove(uuid);
+    }
+
+    public synchronized void removeIndicateCallback(String uuid) {
+        if (bleIndicateCallbackHashMap.containsKey(uuid))
+            bleIndicateCallbackHashMap.remove(uuid);
+    }
+
+    public synchronized void removeWriteCallback(String uuid) {
+        if (bleWriteCallbackHashMap.containsKey(uuid))
+            bleWriteCallbackHashMap.remove(uuid);
+    }
+
+    public synchronized void removeReadCallback(String uuid) {
+        if (bleReadCallbackHashMap.containsKey(uuid))
+            bleReadCallbackHashMap.remove(uuid);
+    }
+
+    public synchronized void clearCharacterCallback() {
+        bleNotifyCallbackHashMap.clear();
+        bleIndicateCallbackHashMap.clear();
+        bleWriteCallbackHashMap.clear();
+        bleReadCallbackHashMap.clear();
+    }
+
+    public synchronized void addRssiCallback(BleRssiCallback callback) {
+        bleRssiCallback = callback;
+    }
+
+    public synchronized void removeRssiCallback() {
+        bleRssiCallback = null;
+    }
+
+    public synchronized void addMtuChangedCallback(BleMtuChangedCallback callback) {
+        bleMtuChangedCallback = callback;
+    }
+
+    public synchronized void removeMtuChangedCallback() {
+        bleMtuChangedCallback = null;
+    }
+
+
+    public String getDeviceKey() {
+        return bleDevice.getKey();
+    }
+
+    public BleDevice getDevice() {
+        return bleDevice;
+    }
+
+    public BluetoothGatt getBluetoothGatt() {
+        return bluetoothGatt;
+    }
+
+    public synchronized BluetoothGatt connect(BleDevice bleDevice,
+                                              boolean autoConnect,
+                                              BleGattCallback callback) {
+        return connect(bleDevice, autoConnect, callback, 0);
+    }
+
+    public synchronized BluetoothGatt connect(BleDevice bleDevice,
+                                              boolean autoConnect,
+                                              BleGattCallback callback,
+                                              int connectRetryCount) {
+        BleLog.i("connect device: " + bleDevice.getName()
+                + "\nmac: " + bleDevice.getMac()
+                + "\nautoConnect: " + autoConnect
+                + "\ncurrentThread: " + Thread.currentThread().getId()
+                + "\nconnectCount:" + (connectRetryCount + 1));
+        if (connectRetryCount == 0) {
+            this.connectRetryCount = 0;
+        }
+
+        addConnectGattCallback(callback);
+
+        lastState = LastState.CONNECT_CONNECTING;
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            bluetoothGatt = bleDevice.getDevice().connectGatt(BleManager.getInstance().getContext(),
+                    autoConnect, coreGattCallback, TRANSPORT_LE);
+        } else {
+            bluetoothGatt = bleDevice.getDevice().connectGatt(BleManager.getInstance().getContext(),
+                    autoConnect, coreGattCallback);
+        }
+        if (bluetoothGatt != null) {
+            if (bleGattCallback != null) {
+                bleGattCallback.onStartConnect();
+            }
+            Message message = mainHandler.obtainMessage();
+            message.what = BleMsg.MSG_CONNECT_OVER_TIME;
+            mainHandler.sendMessageDelayed(message, BleManager.getInstance().getConnectOverTime());
+
+        } else {
+            disconnectGatt();
+            refreshDeviceCache();
+            closeBluetoothGatt();
+            lastState = LastState.CONNECT_FAILURE;
+            BleManager.getInstance().getMultipleBluetoothController().removeConnectingBle(BleBluetooth.this);
+            if (bleGattCallback != null)
+                bleGattCallback.onConnectFail(bleDevice, new OtherException("GATT connect exception occurred!"));
+
+        }
+        return bluetoothGatt;
+    }
+
+    public synchronized void disconnect() {
+        isActiveDisconnect = true;
+        disconnectGatt();
+    }
+
+    public synchronized void destroy() {
+        lastState = LastState.CONNECT_IDLE;
+        disconnectGatt();
+        refreshDeviceCache();
+        closeBluetoothGatt();
+        removeConnectGattCallback();
+        removeRssiCallback();
+        removeMtuChangedCallback();
+        clearCharacterCallback();
+        mainHandler.removeCallbacksAndMessages(null);
+    }
+
+    private synchronized void disconnectGatt() {
+        if (bluetoothGatt != null) {
+            bluetoothGatt.disconnect();
+        }
+    }
+
+    private synchronized void refreshDeviceCache() {
+        try {
+            final Method refresh = BluetoothGatt.class.getMethod("refresh");
+            if (refresh != null && bluetoothGatt != null) {
+                boolean success = (Boolean) refresh.invoke(bluetoothGatt);
+                BleLog.i("refreshDeviceCache, is success:  " + success);
+            }
+        } catch (Exception e) {
+            BleLog.i("exception occur while refreshing device: " + e.getMessage());
+            e.printStackTrace();
+        }
+    }
+
+    private synchronized void closeBluetoothGatt() {
+        if (bluetoothGatt != null) {
+            bluetoothGatt.close();
+        }
+    }
+
+    private final class MainHandler extends Handler {
+
+        MainHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case BleMsg.MSG_CONNECT_FAIL: {
+                    disconnectGatt();
+                    refreshDeviceCache();
+                    closeBluetoothGatt();
+
+                    if (connectRetryCount < BleManager.getInstance().getReConnectCount()) {
+                        BleLog.e("Connect fail, try reconnect " + BleManager.getInstance().getReConnectInterval() + " millisecond later");
+                        ++connectRetryCount;
+
+                        Message message = mainHandler.obtainMessage();
+                        message.what = BleMsg.MSG_RECONNECT;
+                        mainHandler.sendMessageDelayed(message, BleManager.getInstance().getReConnectInterval());
+                    } else {
+                        lastState = LastState.CONNECT_FAILURE;
+                        BleManager.getInstance().getMultipleBluetoothController().removeConnectingBle(BleBluetooth.this);
+
+                        BleConnectStateParameter para = (BleConnectStateParameter) msg.obj;
+                        int status = para.getStatus();
+                        if (bleGattCallback != null)
+                            bleGattCallback.onConnectFail(bleDevice, new ConnectException(bluetoothGatt, status));
+                    }
+                }
+                break;
+
+                case BleMsg.MSG_DISCONNECTED: {
+                    lastState = LastState.CONNECT_DISCONNECT;
+                    BleManager.getInstance().getMultipleBluetoothController().removeBleBluetooth(BleBluetooth.this);
+
+                    disconnect();
+                    refreshDeviceCache();
+                    closeBluetoothGatt();
+                    removeRssiCallback();
+                    removeMtuChangedCallback();
+                    clearCharacterCallback();
+                    mainHandler.removeCallbacksAndMessages(null);
+
+                    BleConnectStateParameter para = (BleConnectStateParameter) msg.obj;
+                    boolean isActive = para.isActive();
+                    int status = para.getStatus();
+                    if (bleGattCallback != null)
+                        bleGattCallback.onDisConnected(isActive, bleDevice, bluetoothGatt, status);
+                }
+                break;
+
+                case BleMsg.MSG_RECONNECT: {
+                    connect(bleDevice, false, bleGattCallback, connectRetryCount);
+                }
+                break;
+
+                case BleMsg.MSG_CONNECT_OVER_TIME: {
+                    disconnectGatt();
+                    refreshDeviceCache();
+                    closeBluetoothGatt();
+
+                    lastState = LastState.CONNECT_FAILURE;
+                    BleManager.getInstance().getMultipleBluetoothController().removeConnectingBle(BleBluetooth.this);
+
+                    if (bleGattCallback != null)
+                        bleGattCallback.onConnectFail(bleDevice, new TimeoutException());
+                }
+                break;
+
+                case BleMsg.MSG_DISCOVER_SERVICES: {
+                    if (bluetoothGatt != null) {
+                        boolean discoverServiceResult = bluetoothGatt.discoverServices();
+                        if (!discoverServiceResult) {
+                            Message message = mainHandler.obtainMessage();
+                            message.what = BleMsg.MSG_DISCOVER_FAIL;
+                            mainHandler.sendMessage(message);
+                        }
+                    } else {
+                        Message message = mainHandler.obtainMessage();
+                        message.what = BleMsg.MSG_DISCOVER_FAIL;
+                        mainHandler.sendMessage(message);
+                    }
+                }
+                break;
+
+                case BleMsg.MSG_DISCOVER_FAIL: {
+                    disconnectGatt();
+                    refreshDeviceCache();
+                    closeBluetoothGatt();
+
+                    lastState = LastState.CONNECT_FAILURE;
+                    BleManager.getInstance().getMultipleBluetoothController().removeConnectingBle(BleBluetooth.this);
+
+                    if (bleGattCallback != null)
+                        bleGattCallback.onConnectFail(bleDevice,
+                                new OtherException("GATT discover services exception occurred!"));
+                }
+                break;
+
+                case BleMsg.MSG_DISCOVER_SUCCESS: {
+                    lastState = LastState.CONNECT_CONNECTED;
+                    isActiveDisconnect = false;
+                    BleManager.getInstance().getMultipleBluetoothController().removeConnectingBle(BleBluetooth.this);
+                    BleManager.getInstance().getMultipleBluetoothController().addBleBluetooth(BleBluetooth.this);
+
+                    BleConnectStateParameter para = (BleConnectStateParameter) msg.obj;
+                    int status = para.getStatus();
+                    if (bleGattCallback != null)
+                        bleGattCallback.onConnectSuccess(bleDevice, bluetoothGatt, status);
+                }
+                break;
+
+                default:
+                    super.handleMessage(msg);
+                    break;
+            }
+        }
+    }
+
+    private BluetoothGattCallback coreGattCallback = new BluetoothGattCallback() {
+
+        @Override
+        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
+            super.onConnectionStateChange(gatt, status, newState);
+            BleLog.i("BluetoothGattCallback:onConnectionStateChange "
+                    + '\n' + "status: " + status
+                    + '\n' + "newState: " + newState
+                    + '\n' + "currentThread: " + Thread.currentThread().getId());
+
+            bluetoothGatt = gatt;
+
+            mainHandler.removeMessages(BleMsg.MSG_CONNECT_OVER_TIME);
+
+            if (newState == BluetoothProfile.STATE_CONNECTED) {
+                Message message = mainHandler.obtainMessage();
+                message.what = BleMsg.MSG_DISCOVER_SERVICES;
+                mainHandler.sendMessageDelayed(message, 500);
+
+            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
+                if (lastState == LastState.CONNECT_CONNECTING) {
+                    Message message = mainHandler.obtainMessage();
+                    message.what = BleMsg.MSG_CONNECT_FAIL;
+                    message.obj = new BleConnectStateParameter(status);
+                    mainHandler.sendMessage(message);
+
+                } else if (lastState == LastState.CONNECT_CONNECTED) {
+                    Message message = mainHandler.obtainMessage();
+                    message.what = BleMsg.MSG_DISCONNECTED;
+                    BleConnectStateParameter para = new BleConnectStateParameter(status);
+                    para.setActive(isActiveDisconnect);
+                    message.obj = para;
+                    mainHandler.sendMessage(message);
+                }
+            }
+        }
+
+        @Override
+        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
+            super.onServicesDiscovered(gatt, status);
+            BleLog.i("BluetoothGattCallback:onServicesDiscovered "
+                    + '\n' + "status: " + status
+                    + '\n' + "currentThread: " + Thread.currentThread().getId());
+
+            bluetoothGatt = gatt;
+
+            if (status == BluetoothGatt.GATT_SUCCESS) {
+                Message message = mainHandler.obtainMessage();
+                message.what = BleMsg.MSG_DISCOVER_SUCCESS;
+                message.obj = new BleConnectStateParameter(status);
+                mainHandler.sendMessage(message);
+
+            } else {
+                Message message = mainHandler.obtainMessage();
+                message.what = BleMsg.MSG_DISCOVER_FAIL;
+                mainHandler.sendMessage(message);
+            }
+        }
+
+        @Override
+        public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
+            super.onCharacteristicChanged(gatt, characteristic);
+
+            Iterator iterator = bleNotifyCallbackHashMap.entrySet().iterator();
+            while (iterator.hasNext()) {
+                Map.Entry entry = (Map.Entry) iterator.next();
+                Object callback = entry.getValue();
+                if (callback instanceof BleNotifyCallback) {
+                    BleNotifyCallback bleNotifyCallback = (BleNotifyCallback) callback;
+                    if (characteristic.getUuid().toString().equalsIgnoreCase(bleNotifyCallback.getKey())) {
+                        Handler handler = bleNotifyCallback.getHandler();
+                        if (handler != null) {
+                            Message message = handler.obtainMessage();
+                            message.what = BleMsg.MSG_CHA_NOTIFY_DATA_CHANGE;
+                            message.obj = bleNotifyCallback;
+                            Bundle bundle = new Bundle();
+                            bundle.putByteArray(BleMsg.KEY_NOTIFY_BUNDLE_VALUE, characteristic.getValue());
+                            message.setData(bundle);
+                            handler.sendMessage(message);
+                        }
+                    }
+                }
+            }
+
+            iterator = bleIndicateCallbackHashMap.entrySet().iterator();
+            while (iterator.hasNext()) {
+                Map.Entry entry = (Map.Entry) iterator.next();
+                Object callback = entry.getValue();
+                if (callback instanceof BleIndicateCallback) {
+                    BleIndicateCallback bleIndicateCallback = (BleIndicateCallback) callback;
+                    if (characteristic.getUuid().toString().equalsIgnoreCase(bleIndicateCallback.getKey())) {
+                        Handler handler = bleIndicateCallback.getHandler();
+                        if (handler != null) {
+                            Message message = handler.obtainMessage();
+                            message.what = BleMsg.MSG_CHA_INDICATE_DATA_CHANGE;
+                            message.obj = bleIndicateCallback;
+                            Bundle bundle = new Bundle();
+                            bundle.putByteArray(BleMsg.KEY_INDICATE_BUNDLE_VALUE, characteristic.getValue());
+                            message.setData(bundle);
+                            handler.sendMessage(message);
+                        }
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
+            super.onDescriptorWrite(gatt, descriptor, status);
+
+            Iterator iterator = bleNotifyCallbackHashMap.entrySet().iterator();
+            while (iterator.hasNext()) {
+                Map.Entry entry = (Map.Entry) iterator.next();
+                Object callback = entry.getValue();
+                if (callback instanceof BleNotifyCallback) {
+                    BleNotifyCallback bleNotifyCallback = (BleNotifyCallback) callback;
+                    if (descriptor.getCharacteristic().getUuid().toString().equalsIgnoreCase(bleNotifyCallback.getKey())) {
+                        Handler handler = bleNotifyCallback.getHandler();
+                        if (handler != null) {
+                            Message message = handler.obtainMessage();
+                            message.what = BleMsg.MSG_CHA_NOTIFY_RESULT;
+                            message.obj = bleNotifyCallback;
+                            Bundle bundle = new Bundle();
+                            bundle.putInt(BleMsg.KEY_NOTIFY_BUNDLE_STATUS, status);
+                            message.setData(bundle);
+                            handler.sendMessage(message);
+                        }
+                    }
+                }
+            }
+
+            iterator = bleIndicateCallbackHashMap.entrySet().iterator();
+            while (iterator.hasNext()) {
+                Map.Entry entry = (Map.Entry) iterator.next();
+                Object callback = entry.getValue();
+                if (callback instanceof BleIndicateCallback) {
+                    BleIndicateCallback bleIndicateCallback = (BleIndicateCallback) callback;
+                    if (descriptor.getCharacteristic().getUuid().toString().equalsIgnoreCase(bleIndicateCallback.getKey())) {
+                        Handler handler = bleIndicateCallback.getHandler();
+                        if (handler != null) {
+                            Message message = handler.obtainMessage();
+                            message.what = BleMsg.MSG_CHA_INDICATE_RESULT;
+                            message.obj = bleIndicateCallback;
+                            Bundle bundle = new Bundle();
+                            bundle.putInt(BleMsg.KEY_INDICATE_BUNDLE_STATUS, status);
+                            message.setData(bundle);
+                            handler.sendMessage(message);
+                        }
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+            super.onCharacteristicWrite(gatt, characteristic, status);
+
+            Iterator iterator = bleWriteCallbackHashMap.entrySet().iterator();
+            while (iterator.hasNext()) {
+                Map.Entry entry = (Map.Entry) iterator.next();
+                Object callback = entry.getValue();
+                if (callback instanceof BleWriteCallback) {
+                    BleWriteCallback bleWriteCallback = (BleWriteCallback) callback;
+                    if (characteristic.getUuid().toString().equalsIgnoreCase(bleWriteCallback.getKey())) {
+                        Handler handler = bleWriteCallback.getHandler();
+                        if (handler != null) {
+                            Message message = handler.obtainMessage();
+                            message.what = BleMsg.MSG_CHA_WRITE_RESULT;
+                            message.obj = bleWriteCallback;
+                            Bundle bundle = new Bundle();
+                            bundle.putInt(BleMsg.KEY_WRITE_BUNDLE_STATUS, status);
+                            bundle.putByteArray(BleMsg.KEY_WRITE_BUNDLE_VALUE, characteristic.getValue());
+                            message.setData(bundle);
+                            handler.sendMessage(message);
+                        }
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
+            super.onCharacteristicRead(gatt, characteristic, status);
+
+            Iterator iterator = bleReadCallbackHashMap.entrySet().iterator();
+            while (iterator.hasNext()) {
+                Map.Entry entry = (Map.Entry) iterator.next();
+                Object callback = entry.getValue();
+                if (callback instanceof BleReadCallback) {
+                    BleReadCallback bleReadCallback = (BleReadCallback) callback;
+                    if (characteristic.getUuid().toString().equalsIgnoreCase(bleReadCallback.getKey())) {
+                        Handler handler = bleReadCallback.getHandler();
+                        if (handler != null) {
+                            Message message = handler.obtainMessage();
+                            message.what = BleMsg.MSG_CHA_READ_RESULT;
+                            message.obj = bleReadCallback;
+                            Bundle bundle = new Bundle();
+                            bundle.putInt(BleMsg.KEY_READ_BUNDLE_STATUS, status);
+                            bundle.putByteArray(BleMsg.KEY_READ_BUNDLE_VALUE, characteristic.getValue());
+                            message.setData(bundle);
+                            handler.sendMessage(message);
+                        }
+                    }
+                }
+            }
+        }
+
+        @Override
+        public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
+            super.onReadRemoteRssi(gatt, rssi, status);
+
+            if (bleRssiCallback != null) {
+                Handler handler = bleRssiCallback.getHandler();
+                if (handler != null) {
+                    Message message = handler.obtainMessage();
+                    message.what = BleMsg.MSG_READ_RSSI_RESULT;
+                    message.obj = bleRssiCallback;
+                    Bundle bundle = new Bundle();
+                    bundle.putInt(BleMsg.KEY_READ_RSSI_BUNDLE_STATUS, status);
+                    bundle.putInt(BleMsg.KEY_READ_RSSI_BUNDLE_VALUE, rssi);
+                    message.setData(bundle);
+                    handler.sendMessage(message);
+                }
+            }
+        }
+
+        @Override
+        public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
+            super.onMtuChanged(gatt, mtu, status);
+
+            if (bleMtuChangedCallback != null) {
+                Handler handler = bleMtuChangedCallback.getHandler();
+                if (handler != null) {
+                    Message message = handler.obtainMessage();
+                    message.what = BleMsg.MSG_SET_MTU_RESULT;
+                    message.obj = bleMtuChangedCallback;
+                    Bundle bundle = new Bundle();
+                    bundle.putInt(BleMsg.KEY_SET_MTU_BUNDLE_STATUS, status);
+                    bundle.putInt(BleMsg.KEY_SET_MTU_BUNDLE_VALUE, mtu);
+                    message.setData(bundle);
+                    handler.sendMessage(message);
+                }
+            }
+        }
+    };
+
+    enum LastState {
+        CONNECT_IDLE,
+        CONNECT_CONNECTING,
+        CONNECT_CONNECTED,
+        CONNECT_FAILURE,
+        CONNECT_DISCONNECT
+    }
+
+}

+ 608 - 0
FastBleLib/src/main/java/com/clj/fastble/bluetooth/BleConnector.java

@@ -0,0 +1,608 @@
+
+package com.clj.fastble.bluetooth;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import com.clj.fastble.BleManager;
+import com.clj.fastble.callback.BleIndicateCallback;
+import com.clj.fastble.callback.BleMtuChangedCallback;
+import com.clj.fastble.callback.BleNotifyCallback;
+import com.clj.fastble.callback.BleReadCallback;
+import com.clj.fastble.callback.BleRssiCallback;
+import com.clj.fastble.callback.BleWriteCallback;
+import com.clj.fastble.data.BleMsg;
+import com.clj.fastble.data.BleWriteState;
+import com.clj.fastble.exception.GattException;
+import com.clj.fastble.exception.OtherException;
+import com.clj.fastble.exception.TimeoutException;
+
+import java.util.UUID;
+
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public class BleConnector {
+
+    private static final String UUID_CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR = "00002902-0000-1000-8000-00805f9b34fb";
+
+    private BluetoothGatt mBluetoothGatt;
+    private BluetoothGattService mGattService;
+    private BluetoothGattCharacteristic mCharacteristic;
+    private BleBluetooth mBleBluetooth;
+    private Handler mHandler;
+
+    BleConnector(BleBluetooth bleBluetooth) {
+        this.mBleBluetooth = bleBluetooth;
+        this.mBluetoothGatt = bleBluetooth.getBluetoothGatt();
+        this.mHandler = new Handler(Looper.getMainLooper()) {
+            @Override
+            public void handleMessage(Message msg) {
+                super.handleMessage(msg);
+                switch (msg.what) {
+
+                    case BleMsg.MSG_CHA_NOTIFY_START: {
+                        BleNotifyCallback notifyCallback = (BleNotifyCallback) msg.obj;
+                        if (notifyCallback != null)
+                            notifyCallback.onNotifyFailure(new TimeoutException());
+                        break;
+                    }
+
+                    case BleMsg.MSG_CHA_NOTIFY_RESULT: {
+                        notifyMsgInit();
+
+                        BleNotifyCallback notifyCallback = (BleNotifyCallback) msg.obj;
+                        Bundle bundle = msg.getData();
+                        int status = bundle.getInt(BleMsg.KEY_NOTIFY_BUNDLE_STATUS);
+                        if (notifyCallback != null) {
+                            if (status == BluetoothGatt.GATT_SUCCESS) {
+                                notifyCallback.onNotifySuccess();
+                            } else {
+                                notifyCallback.onNotifyFailure(new GattException(status));
+                            }
+                        }
+                        break;
+                    }
+
+                    case BleMsg.MSG_CHA_NOTIFY_DATA_CHANGE: {
+                        BleNotifyCallback notifyCallback = (BleNotifyCallback) msg.obj;
+                        Bundle bundle = msg.getData();
+                        byte[] value = bundle.getByteArray(BleMsg.KEY_NOTIFY_BUNDLE_VALUE);
+                        if (notifyCallback != null) {
+                            notifyCallback.onCharacteristicChanged(value);
+                        }
+                        break;
+                    }
+
+                    case BleMsg.MSG_CHA_INDICATE_START: {
+                        BleIndicateCallback indicateCallback = (BleIndicateCallback) msg.obj;
+                        if (indicateCallback != null)
+                            indicateCallback.onIndicateFailure(new TimeoutException());
+                        break;
+                    }
+
+                    case BleMsg.MSG_CHA_INDICATE_RESULT: {
+                        indicateMsgInit();
+
+                        BleIndicateCallback indicateCallback = (BleIndicateCallback) msg.obj;
+                        Bundle bundle = msg.getData();
+                        int status = bundle.getInt(BleMsg.KEY_INDICATE_BUNDLE_STATUS);
+                        if (indicateCallback != null) {
+                            if (status == BluetoothGatt.GATT_SUCCESS) {
+                                indicateCallback.onIndicateSuccess();
+                            } else {
+                                indicateCallback.onIndicateFailure(new GattException(status));
+                            }
+                        }
+                        break;
+                    }
+
+                    case BleMsg.MSG_CHA_INDICATE_DATA_CHANGE: {
+                        BleIndicateCallback indicateCallback = (BleIndicateCallback) msg.obj;
+                        Bundle bundle = msg.getData();
+                        byte[] value = bundle.getByteArray(BleMsg.KEY_INDICATE_BUNDLE_VALUE);
+                        if (indicateCallback != null) {
+                            indicateCallback.onCharacteristicChanged(value);
+                        }
+                        break;
+                    }
+
+                    case BleMsg.MSG_CHA_WRITE_START: {
+                        BleWriteCallback writeCallback = (BleWriteCallback) msg.obj;
+                        if (writeCallback != null) {
+                            writeCallback.onWriteFailure(new TimeoutException());
+                        }
+                        break;
+                    }
+
+                    case BleMsg.MSG_CHA_WRITE_RESULT: {
+                        writeMsgInit();
+
+                        BleWriteCallback writeCallback = (BleWriteCallback) msg.obj;
+                        Bundle bundle = msg.getData();
+                        int status = bundle.getInt(BleMsg.KEY_WRITE_BUNDLE_STATUS);
+                        byte[] value = bundle.getByteArray(BleMsg.KEY_WRITE_BUNDLE_VALUE);
+                        if (writeCallback != null) {
+                            if (status == BluetoothGatt.GATT_SUCCESS) {
+                                writeCallback.onWriteSuccess(BleWriteState.DATA_WRITE_SINGLE, BleWriteState.DATA_WRITE_SINGLE, value);
+                            } else {
+                                writeCallback.onWriteFailure(new GattException(status));
+                            }
+                        }
+                        break;
+                    }
+
+                    case BleMsg.MSG_CHA_READ_START: {
+                        BleReadCallback readCallback = (BleReadCallback) msg.obj;
+                        if (readCallback != null)
+                            readCallback.onReadFailure(new TimeoutException());
+                        break;
+                    }
+
+                    case BleMsg.MSG_CHA_READ_RESULT: {
+                        readMsgInit();
+
+                        BleReadCallback readCallback = (BleReadCallback) msg.obj;
+                        Bundle bundle = msg.getData();
+                        int status = bundle.getInt(BleMsg.KEY_READ_BUNDLE_STATUS);
+                        byte[] value = bundle.getByteArray(BleMsg.KEY_READ_BUNDLE_VALUE);
+                        if (readCallback != null) {
+                            if (status == BluetoothGatt.GATT_SUCCESS) {
+                                readCallback.onReadSuccess(value);
+                            } else {
+                                readCallback.onReadFailure(new GattException(status));
+                            }
+                        }
+                        break;
+                    }
+
+                    case BleMsg.MSG_READ_RSSI_START: {
+                        BleRssiCallback rssiCallback = (BleRssiCallback) msg.obj;
+                        if (rssiCallback != null)
+                            rssiCallback.onRssiFailure(new TimeoutException());
+                        break;
+                    }
+
+                    case BleMsg.MSG_READ_RSSI_RESULT: {
+                        rssiMsgInit();
+
+                        BleRssiCallback rssiCallback = (BleRssiCallback) msg.obj;
+                        Bundle bundle = msg.getData();
+                        int status = bundle.getInt(BleMsg.KEY_READ_RSSI_BUNDLE_STATUS);
+                        int value = bundle.getInt(BleMsg.KEY_READ_RSSI_BUNDLE_VALUE);
+                        if (rssiCallback != null) {
+                            if (status == BluetoothGatt.GATT_SUCCESS) {
+                                rssiCallback.onRssiSuccess(value);
+                            } else {
+                                rssiCallback.onRssiFailure(new GattException(status));
+                            }
+                        }
+                        break;
+                    }
+
+                    case BleMsg.MSG_SET_MTU_START: {
+                        BleMtuChangedCallback mtuChangedCallback = (BleMtuChangedCallback) msg.obj;
+                        if (mtuChangedCallback != null)
+                            mtuChangedCallback.onSetMTUFailure(new TimeoutException());
+                        break;
+                    }
+
+                    case BleMsg.MSG_SET_MTU_RESULT: {
+                        mtuChangedMsgInit();
+
+                        BleMtuChangedCallback mtuChangedCallback = (BleMtuChangedCallback) msg.obj;
+                        Bundle bundle = msg.getData();
+                        int status = bundle.getInt(BleMsg.KEY_SET_MTU_BUNDLE_STATUS);
+                        int value = bundle.getInt(BleMsg.KEY_SET_MTU_BUNDLE_VALUE);
+                        if (mtuChangedCallback != null) {
+                            if (status == BluetoothGatt.GATT_SUCCESS) {
+                                mtuChangedCallback.onMtuChanged(value);
+                            } else {
+                                mtuChangedCallback.onSetMTUFailure(new GattException(status));
+                            }
+                        }
+                        break;
+                    }
+                }
+            }
+        };
+
+    }
+
+    private BleConnector withUUID(UUID serviceUUID, UUID characteristicUUID) {
+        if (serviceUUID != null && mBluetoothGatt != null) {
+            mGattService = mBluetoothGatt.getService(serviceUUID);
+        }
+        if (mGattService != null && characteristicUUID != null) {
+            mCharacteristic = mGattService.getCharacteristic(characteristicUUID);
+        }
+        return this;
+    }
+
+    public BleConnector withUUIDString(String serviceUUID, String characteristicUUID) {
+        return withUUID(formUUID(serviceUUID), formUUID(characteristicUUID));
+    }
+
+    private UUID formUUID(String uuid) {
+        return uuid == null ? null : UUID.fromString(uuid);
+    }
+
+
+    /*------------------------------- main operation ----------------------------------- */
+
+
+    /**
+     * notify
+     */
+    public void enableCharacteristicNotify(BleNotifyCallback bleNotifyCallback, String uuid_notify,
+                                           boolean userCharacteristicDescriptor) {
+        if (mCharacteristic != null
+                && (mCharacteristic.getProperties() | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
+
+            handleCharacteristicNotifyCallback(bleNotifyCallback, uuid_notify);
+            setCharacteristicNotification(mBluetoothGatt, mCharacteristic, userCharacteristicDescriptor, true, bleNotifyCallback);
+        } else {
+            if (bleNotifyCallback != null)
+                bleNotifyCallback.onNotifyFailure(new OtherException("this characteristic not support notify!"));
+        }
+    }
+
+    /**
+     * stop notify
+     */
+    public boolean disableCharacteristicNotify(boolean useCharacteristicDescriptor) {
+        if (mCharacteristic != null
+                && (mCharacteristic.getProperties() | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
+            return setCharacteristicNotification(mBluetoothGatt, mCharacteristic,
+                    useCharacteristicDescriptor, false, null);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * notify setting
+     */
+    private boolean setCharacteristicNotification(BluetoothGatt gatt,
+                                                  BluetoothGattCharacteristic characteristic,
+                                                  boolean useCharacteristicDescriptor,
+                                                  boolean enable,
+                                                  BleNotifyCallback bleNotifyCallback) {
+        if (gatt == null || characteristic == null) {
+            notifyMsgInit();
+            if (bleNotifyCallback != null)
+                bleNotifyCallback.onNotifyFailure(new OtherException("gatt or characteristic equal null"));
+            return false;
+        }
+
+        boolean success1 = gatt.setCharacteristicNotification(characteristic, enable);
+        if (!success1) {
+            notifyMsgInit();
+            if (bleNotifyCallback != null)
+                bleNotifyCallback.onNotifyFailure(new OtherException("gatt setCharacteristicNotification fail"));
+            return false;
+        }
+
+        BluetoothGattDescriptor descriptor;
+        if (useCharacteristicDescriptor) {
+            descriptor = characteristic.getDescriptor(characteristic.getUuid());
+        } else {
+            descriptor = characteristic.getDescriptor(formUUID(UUID_CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR));
+        }
+        if (descriptor == null) {
+            notifyMsgInit();
+            if (bleNotifyCallback != null)
+                bleNotifyCallback.onNotifyFailure(new OtherException("descriptor equals null"));
+            return false;
+        } else {
+            descriptor.setValue(enable ? BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE :
+                    BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
+            boolean success2 = gatt.writeDescriptor(descriptor);
+            if (!success2) {
+                notifyMsgInit();
+                if (bleNotifyCallback != null)
+                    bleNotifyCallback.onNotifyFailure(new OtherException("gatt writeDescriptor fail"));
+            }
+            return success2;
+        }
+    }
+
+    /**
+     * indicate
+     */
+    public void enableCharacteristicIndicate(BleIndicateCallback bleIndicateCallback, String uuid_indicate,
+                                             boolean useCharacteristicDescriptor) {
+        if (mCharacteristic != null
+                && (mCharacteristic.getProperties() | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
+            handleCharacteristicIndicateCallback(bleIndicateCallback, uuid_indicate);
+            setCharacteristicIndication(mBluetoothGatt, mCharacteristic,
+                    useCharacteristicDescriptor, true, bleIndicateCallback);
+        } else {
+            if (bleIndicateCallback != null)
+                bleIndicateCallback.onIndicateFailure(new OtherException("this characteristic not support indicate!"));
+        }
+    }
+
+
+    /**
+     * stop indicate
+     */
+    public boolean disableCharacteristicIndicate(boolean userCharacteristicDescriptor) {
+        if (mCharacteristic != null
+                && (mCharacteristic.getProperties() | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) {
+            return setCharacteristicIndication(mBluetoothGatt, mCharacteristic,
+                    userCharacteristicDescriptor, false, null);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * indicate setting
+     */
+    private boolean setCharacteristicIndication(BluetoothGatt gatt,
+                                                BluetoothGattCharacteristic characteristic,
+                                                boolean useCharacteristicDescriptor,
+                                                boolean enable,
+                                                BleIndicateCallback bleIndicateCallback) {
+        if (gatt == null || characteristic == null) {
+            indicateMsgInit();
+            if (bleIndicateCallback != null)
+                bleIndicateCallback.onIndicateFailure(new OtherException("gatt or characteristic equal null"));
+            return false;
+        }
+
+        boolean success1 = gatt.setCharacteristicNotification(characteristic, enable);
+        if (!success1) {
+            indicateMsgInit();
+            if (bleIndicateCallback != null)
+                bleIndicateCallback.onIndicateFailure(new OtherException("gatt setCharacteristicNotification fail"));
+            return false;
+        }
+
+        BluetoothGattDescriptor descriptor;
+        if (useCharacteristicDescriptor) {
+            descriptor = characteristic.getDescriptor(characteristic.getUuid());
+        } else {
+            descriptor = characteristic.getDescriptor(formUUID(UUID_CLIENT_CHARACTERISTIC_CONFIG_DESCRIPTOR));
+        }
+        if (descriptor == null) {
+            indicateMsgInit();
+            if (bleIndicateCallback != null)
+                bleIndicateCallback.onIndicateFailure(new OtherException("descriptor equals null"));
+            return false;
+        } else {
+            descriptor.setValue(enable ? BluetoothGattDescriptor.ENABLE_INDICATION_VALUE :
+                    BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE);
+            boolean success2 = gatt.writeDescriptor(descriptor);
+            if (!success2) {
+                indicateMsgInit();
+                if (bleIndicateCallback != null)
+                    bleIndicateCallback.onIndicateFailure(new OtherException("gatt writeDescriptor fail"));
+            }
+            return success2;
+        }
+    }
+
+    /**
+     * write
+     */
+    public void writeCharacteristic(byte[] data, BleWriteCallback bleWriteCallback, String uuid_write) {
+        if (data == null || data.length <= 0) {
+            if (bleWriteCallback != null)
+                bleWriteCallback.onWriteFailure(new OtherException("the data to be written is empty"));
+            return;
+        }
+
+        if (mCharacteristic == null
+                || (mCharacteristic.getProperties() & (BluetoothGattCharacteristic.PROPERTY_WRITE | BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE)) == 0) {
+            if (bleWriteCallback != null)
+                bleWriteCallback.onWriteFailure(new OtherException("this characteristic not support write!"));
+            return;
+        }
+
+        if (mCharacteristic.setValue(data)) {
+            handleCharacteristicWriteCallback(bleWriteCallback, uuid_write);
+            if (!mBluetoothGatt.writeCharacteristic(mCharacteristic)) {
+                writeMsgInit();
+                if (bleWriteCallback != null)
+                    bleWriteCallback.onWriteFailure(new OtherException("gatt writeCharacteristic fail"));
+            }
+        } else {
+            if (bleWriteCallback != null)
+                bleWriteCallback.onWriteFailure(new OtherException("Updates the locally stored value of this characteristic fail"));
+        }
+    }
+
+    /**
+     * read
+     */
+    public void readCharacteristic(BleReadCallback bleReadCallback, String uuid_read) {
+        if (mCharacteristic != null
+                && (mCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
+
+            handleCharacteristicReadCallback(bleReadCallback, uuid_read);
+            if (!mBluetoothGatt.readCharacteristic(mCharacteristic)) {
+                readMsgInit();
+                if (bleReadCallback != null)
+                    bleReadCallback.onReadFailure(new OtherException("gatt readCharacteristic fail"));
+            }
+        } else {
+            if (bleReadCallback != null)
+                bleReadCallback.onReadFailure(new OtherException("this characteristic not support read!"));
+        }
+    }
+
+    /**
+     * rssi
+     */
+    public void readRemoteRssi(BleRssiCallback bleRssiCallback) {
+        handleRSSIReadCallback(bleRssiCallback);
+        if (!mBluetoothGatt.readRemoteRssi()) {
+            rssiMsgInit();
+            if (bleRssiCallback != null)
+                bleRssiCallback.onRssiFailure(new OtherException("gatt readRemoteRssi fail"));
+        }
+    }
+
+    /**
+     * set mtu
+     */
+    public void setMtu(int requiredMtu, BleMtuChangedCallback bleMtuChangedCallback) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            handleSetMtuCallback(bleMtuChangedCallback);
+            if (!mBluetoothGatt.requestMtu(requiredMtu)) {
+                mtuChangedMsgInit();
+                if (bleMtuChangedCallback != null)
+                    bleMtuChangedCallback.onSetMTUFailure(new OtherException("gatt requestMtu fail"));
+            }
+        } else {
+            if (bleMtuChangedCallback != null)
+                bleMtuChangedCallback.onSetMTUFailure(new OtherException("API level lower than 21"));
+        }
+    }
+
+    /**
+     * requestConnectionPriority
+     *
+     * @param connectionPriority Request a specific connection priority. Must be one of
+     *                           {@link BluetoothGatt#CONNECTION_PRIORITY_BALANCED},
+     *                           {@link BluetoothGatt#CONNECTION_PRIORITY_HIGH}
+     *                           or {@link BluetoothGatt#CONNECTION_PRIORITY_LOW_POWER}.
+     * @throws IllegalArgumentException If the parameters are outside of their
+     *                                  specified range.
+     */
+    public boolean requestConnectionPriority(int connectionPriority) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            return mBluetoothGatt.requestConnectionPriority(connectionPriority);
+        }
+        return false;
+    }
+
+
+    /**************************************** Handle call back ******************************************/
+
+    /**
+     * notify
+     */
+    private void handleCharacteristicNotifyCallback(BleNotifyCallback bleNotifyCallback,
+                                                    String uuid_notify) {
+        if (bleNotifyCallback != null) {
+            notifyMsgInit();
+            bleNotifyCallback.setKey(uuid_notify);
+            bleNotifyCallback.setHandler(mHandler);
+            mBleBluetooth.addNotifyCallback(uuid_notify, bleNotifyCallback);
+            mHandler.sendMessageDelayed(
+                    mHandler.obtainMessage(BleMsg.MSG_CHA_NOTIFY_START, bleNotifyCallback),
+                    BleManager.getInstance().getOperateTimeout());
+        }
+    }
+
+    /**
+     * indicate
+     */
+    private void handleCharacteristicIndicateCallback(BleIndicateCallback bleIndicateCallback,
+                                                      String uuid_indicate) {
+        if (bleIndicateCallback != null) {
+            indicateMsgInit();
+            bleIndicateCallback.setKey(uuid_indicate);
+            bleIndicateCallback.setHandler(mHandler);
+            mBleBluetooth.addIndicateCallback(uuid_indicate, bleIndicateCallback);
+            mHandler.sendMessageDelayed(
+                    mHandler.obtainMessage(BleMsg.MSG_CHA_INDICATE_START, bleIndicateCallback),
+                    BleManager.getInstance().getOperateTimeout());
+        }
+    }
+
+    /**
+     * write
+     */
+    private void handleCharacteristicWriteCallback(BleWriteCallback bleWriteCallback,
+                                                   String uuid_write) {
+        if (bleWriteCallback != null) {
+            writeMsgInit();
+            bleWriteCallback.setKey(uuid_write);
+            bleWriteCallback.setHandler(mHandler);
+            mBleBluetooth.addWriteCallback(uuid_write, bleWriteCallback);
+            mHandler.sendMessageDelayed(
+                    mHandler.obtainMessage(BleMsg.MSG_CHA_WRITE_START, bleWriteCallback),
+                    BleManager.getInstance().getOperateTimeout());
+        }
+    }
+
+    /**
+     * read
+     */
+    private void handleCharacteristicReadCallback(BleReadCallback bleReadCallback,
+                                                  String uuid_read) {
+        if (bleReadCallback != null) {
+            readMsgInit();
+            bleReadCallback.setKey(uuid_read);
+            bleReadCallback.setHandler(mHandler);
+            mBleBluetooth.addReadCallback(uuid_read, bleReadCallback);
+            mHandler.sendMessageDelayed(
+                    mHandler.obtainMessage(BleMsg.MSG_CHA_READ_START, bleReadCallback),
+                    BleManager.getInstance().getOperateTimeout());
+        }
+    }
+
+    /**
+     * rssi
+     */
+    private void handleRSSIReadCallback(BleRssiCallback bleRssiCallback) {
+        if (bleRssiCallback != null) {
+            rssiMsgInit();
+            bleRssiCallback.setHandler(mHandler);
+            mBleBluetooth.addRssiCallback(bleRssiCallback);
+            mHandler.sendMessageDelayed(
+                    mHandler.obtainMessage(BleMsg.MSG_READ_RSSI_START, bleRssiCallback),
+                    BleManager.getInstance().getOperateTimeout());
+        }
+    }
+
+    /**
+     * set mtu
+     */
+    private void handleSetMtuCallback(BleMtuChangedCallback bleMtuChangedCallback) {
+        if (bleMtuChangedCallback != null) {
+            mtuChangedMsgInit();
+            bleMtuChangedCallback.setHandler(mHandler);
+            mBleBluetooth.addMtuChangedCallback(bleMtuChangedCallback);
+            mHandler.sendMessageDelayed(
+                    mHandler.obtainMessage(BleMsg.MSG_SET_MTU_START, bleMtuChangedCallback),
+                    BleManager.getInstance().getOperateTimeout());
+        }
+    }
+
+    public void notifyMsgInit() {
+        mHandler.removeMessages(BleMsg.MSG_CHA_NOTIFY_START);
+    }
+
+    public void indicateMsgInit() {
+        mHandler.removeMessages(BleMsg.MSG_CHA_INDICATE_START);
+    }
+
+    public void writeMsgInit() {
+        mHandler.removeMessages(BleMsg.MSG_CHA_WRITE_START);
+    }
+
+    public void readMsgInit() {
+        mHandler.removeMessages(BleMsg.MSG_CHA_READ_START);
+    }
+
+    public void rssiMsgInit() {
+        mHandler.removeMessages(BleMsg.MSG_READ_RSSI_START);
+    }
+
+    public void mtuChangedMsgInit() {
+        mHandler.removeMessages(BleMsg.MSG_SET_MTU_START);
+    }
+
+}

+ 139 - 0
FastBleLib/src/main/java/com/clj/fastble/bluetooth/MultipleBluetoothController.java

@@ -0,0 +1,139 @@
+package com.clj.fastble.bluetooth;
+
+
+import android.bluetooth.BluetoothDevice;
+import android.os.Build;
+
+import com.clj.fastble.BleManager;
+import com.clj.fastble.data.BleDevice;
+import com.clj.fastble.utils.BleLruHashMap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class MultipleBluetoothController {
+
+    private final BleLruHashMap<String, BleBluetooth> bleLruHashMap;
+    private final HashMap<String, BleBluetooth> bleTempHashMap;
+
+    public MultipleBluetoothController() {
+        bleLruHashMap = new BleLruHashMap<>(BleManager.getInstance().getMaxConnectCount());
+        bleTempHashMap = new HashMap<>();
+    }
+
+    public synchronized BleBluetooth buildConnectingBle(BleDevice bleDevice) {
+        BleBluetooth bleBluetooth = new BleBluetooth(bleDevice);
+        if (!bleTempHashMap.containsKey(bleBluetooth.getDeviceKey())) {
+            bleTempHashMap.put(bleBluetooth.getDeviceKey(), bleBluetooth);
+        }
+        return bleBluetooth;
+    }
+
+    public synchronized void removeConnectingBle(BleBluetooth bleBluetooth) {
+        if (bleBluetooth == null) {
+            return;
+        }
+        if (bleTempHashMap.containsKey(bleBluetooth.getDeviceKey())) {
+            bleTempHashMap.remove(bleBluetooth.getDeviceKey());
+        }
+    }
+
+    public synchronized void addBleBluetooth(BleBluetooth bleBluetooth) {
+        if (bleBluetooth == null) {
+            return;
+        }
+        if (!bleLruHashMap.containsKey(bleBluetooth.getDeviceKey())) {
+            bleLruHashMap.put(bleBluetooth.getDeviceKey(), bleBluetooth);
+        }
+    }
+
+    public synchronized void removeBleBluetooth(BleBluetooth bleBluetooth) {
+        if (bleBluetooth == null) {
+            return;
+        }
+        if (bleLruHashMap.containsKey(bleBluetooth.getDeviceKey())) {
+            bleLruHashMap.remove(bleBluetooth.getDeviceKey());
+        }
+    }
+
+    public synchronized boolean isContainDevice(BleDevice bleDevice) {
+        return bleDevice != null && bleLruHashMap.containsKey(bleDevice.getKey());
+    }
+
+    public synchronized boolean isContainDevice(BluetoothDevice bluetoothDevice) {
+        return bluetoothDevice != null && bleLruHashMap.containsKey(bluetoothDevice.getName() + bluetoothDevice.getAddress());
+    }
+
+    public synchronized BleBluetooth getBleBluetooth(BleDevice bleDevice) {
+        if (bleDevice != null) {
+            if (bleLruHashMap.containsKey(bleDevice.getKey())) {
+                return bleLruHashMap.get(bleDevice.getKey());
+            }
+        }
+        return null;
+    }
+
+    public synchronized void disconnect(BleDevice bleDevice) {
+        if (isContainDevice(bleDevice)) {
+            getBleBluetooth(bleDevice).disconnect();
+        }
+    }
+
+    public synchronized void disconnectAllDevice() {
+        for (Map.Entry<String, BleBluetooth> stringBleBluetoothEntry : bleLruHashMap.entrySet()) {
+            stringBleBluetoothEntry.getValue().disconnect();
+        }
+        bleLruHashMap.clear();
+    }
+
+    public synchronized void destroy() {
+        for (Map.Entry<String, BleBluetooth> stringBleBluetoothEntry : bleLruHashMap.entrySet()) {
+            stringBleBluetoothEntry.getValue().destroy();
+        }
+        bleLruHashMap.clear();
+        for (Map.Entry<String, BleBluetooth> stringBleBluetoothEntry : bleTempHashMap.entrySet()) {
+            stringBleBluetoothEntry.getValue().destroy();
+        }
+        bleTempHashMap.clear();
+    }
+
+    public synchronized List<BleBluetooth> getBleBluetoothList() {
+        List<BleBluetooth> bleBluetoothList = new ArrayList<>(bleLruHashMap.values());
+        Collections.sort(bleBluetoothList, new Comparator<BleBluetooth>() {
+            @Override
+            public int compare(BleBluetooth lhs, BleBluetooth rhs) {
+                return lhs.getDeviceKey().compareToIgnoreCase(rhs.getDeviceKey());
+            }
+        });
+        return bleBluetoothList;
+    }
+
+    public synchronized List<BleDevice> getDeviceList() {
+        refreshConnectedDevice();
+        List<BleDevice> deviceList = new ArrayList<>();
+        for (BleBluetooth BleBluetooth : getBleBluetoothList()) {
+            if (BleBluetooth != null) {
+                deviceList.add(BleBluetooth.getDevice());
+            }
+        }
+        return deviceList;
+    }
+
+    public void refreshConnectedDevice() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+            List<BleBluetooth> bluetoothList = getBleBluetoothList();
+            for (int i = 0; bluetoothList != null && i < bluetoothList.size(); i++) {
+                BleBluetooth bleBluetooth = bluetoothList.get(i);
+                if (!BleManager.getInstance().isConnected(bleBluetooth.getDevice())) {
+                    removeBleBluetooth(bleBluetooth);
+                }
+            }
+        }
+    }
+
+
+}

+ 158 - 0
FastBleLib/src/main/java/com/clj/fastble/bluetooth/SplitWriter.java

@@ -0,0 +1,158 @@
+package com.clj.fastble.bluetooth;
+
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+
+import com.clj.fastble.BleManager;
+import com.clj.fastble.callback.BleWriteCallback;
+import com.clj.fastble.data.BleMsg;
+import com.clj.fastble.exception.BleException;
+import com.clj.fastble.exception.OtherException;
+import com.clj.fastble.utils.BleLog;
+
+import java.util.LinkedList;
+import java.util.Queue;
+
+public class SplitWriter {
+
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+
+    private BleBluetooth mBleBluetooth;
+    private String mUuid_service;
+    private String mUuid_write;
+    private byte[] mData;
+    private int mCount;
+    private boolean mSendNextWhenLastSuccess;
+    private long mIntervalBetweenTwoPackage;
+    private BleWriteCallback mCallback;
+    private Queue<byte[]> mDataQueue;
+    private int mTotalNum;
+
+    public SplitWriter() {
+        mHandlerThread = new HandlerThread("splitWriter");
+        mHandlerThread.start();
+
+        mHandler = new Handler(mHandlerThread.getLooper()) {
+            @Override
+            public void handleMessage(Message msg) {
+                super.handleMessage(msg);
+                if (msg.what == BleMsg.MSG_SPLIT_WRITE_NEXT) {
+                    write();
+                }
+            }
+        };
+    }
+
+    public void splitWrite(BleBluetooth bleBluetooth,
+                           String uuid_service,
+                           String uuid_write,
+                           byte[] data,
+                           boolean sendNextWhenLastSuccess,
+                           long intervalBetweenTwoPackage,
+                           BleWriteCallback callback) {
+        mBleBluetooth = bleBluetooth;
+        mUuid_service = uuid_service;
+        mUuid_write = uuid_write;
+        mData = data;
+        mSendNextWhenLastSuccess = sendNextWhenLastSuccess;
+        mIntervalBetweenTwoPackage = intervalBetweenTwoPackage;
+        mCount = BleManager.getInstance().getSplitWriteNum();
+        mCallback = callback;
+
+        splitWrite();
+    }
+
+    private void splitWrite() {
+        if (mData == null) {
+            throw new IllegalArgumentException("data is Null!");
+        }
+        if (mCount < 1) {
+            throw new IllegalArgumentException("split count should higher than 0!");
+        }
+        mDataQueue = splitByte(mData, mCount);
+        mTotalNum = mDataQueue.size();
+        write();
+    }
+
+    private void write() {
+        if (mDataQueue.peek() == null) {
+            release();
+            return;
+        }
+
+        byte[] data = mDataQueue.poll();
+        mBleBluetooth.newBleConnector()
+                .withUUIDString(mUuid_service, mUuid_write)
+                .writeCharacteristic(
+                        data,
+                        new BleWriteCallback() {
+                            @Override
+                            public void onWriteSuccess(int current, int total, byte[] justWrite) {
+                                int position = mTotalNum - mDataQueue.size();
+                                if (mCallback != null) {
+                                    mCallback.onWriteSuccess(position, mTotalNum, justWrite);
+                                }
+                                if (mSendNextWhenLastSuccess) {
+                                    Message message = mHandler.obtainMessage(BleMsg.MSG_SPLIT_WRITE_NEXT);
+                                    mHandler.sendMessageDelayed(message, mIntervalBetweenTwoPackage);
+                                }
+                            }
+
+                            @Override
+                            public void onWriteFailure(BleException exception) {
+                                if (mCallback != null) {
+                                    mCallback.onWriteFailure(new OtherException("exception occur while writing: " + exception.getDescription()));
+                                }
+                                if (mSendNextWhenLastSuccess) {
+                                    Message message = mHandler.obtainMessage(BleMsg.MSG_SPLIT_WRITE_NEXT);
+                                    mHandler.sendMessageDelayed(message, mIntervalBetweenTwoPackage);
+                                }
+                            }
+                        },
+                        mUuid_write);
+
+        if (!mSendNextWhenLastSuccess) {
+            Message message = mHandler.obtainMessage(BleMsg.MSG_SPLIT_WRITE_NEXT);
+            mHandler.sendMessageDelayed(message, mIntervalBetweenTwoPackage);
+        }
+    }
+
+    private void release() {
+        mHandlerThread.quit();
+        mHandler.removeCallbacksAndMessages(null);
+    }
+
+    private static Queue<byte[]> splitByte(byte[] data, int count) {
+        if (count > 20) {
+            BleLog.w("Be careful: split count beyond 20! Ensure MTU higher than 23!");
+        }
+        Queue<byte[]> byteQueue = new LinkedList<>();
+        int pkgCount;
+        if (data.length % count == 0) {
+            pkgCount = data.length / count;
+        } else {
+            pkgCount = Math.round(data.length / count + 1);
+        }
+
+        if (pkgCount > 0) {
+            for (int i = 0; i < pkgCount; i++) {
+                byte[] dataPkg;
+                int j;
+                if (pkgCount == 1 || i == pkgCount - 1) {
+                    j = data.length % count == 0 ? count : data.length % count;
+                    System.arraycopy(data, i * count, dataPkg = new byte[j], 0, j);
+                } else {
+                    System.arraycopy(data, i * count, dataPkg = new byte[count], 0, count);
+                }
+                byteQueue.offer(dataPkg);
+            }
+        }
+
+        return byteQueue;
+    }
+
+
+}

+ 27 - 0
FastBleLib/src/main/java/com/clj/fastble/callback/BleBaseCallback.java

@@ -0,0 +1,27 @@
+package com.clj.fastble.callback;
+
+
+import android.os.Handler;
+
+public abstract class BleBaseCallback {
+
+    private String key;
+    private Handler handler;
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public Handler getHandler() {
+        return handler;
+    }
+
+    public void setHandler(Handler handler) {
+        this.handler = handler;
+    }
+
+}

+ 24 - 0
FastBleLib/src/main/java/com/clj/fastble/callback/BleGattCallback.java

@@ -0,0 +1,24 @@
+
+package com.clj.fastble.callback;
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCallback;
+import android.os.Build;
+
+import com.clj.fastble.data.BleDevice;
+import com.clj.fastble.exception.BleException;
+
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public abstract class BleGattCallback extends BluetoothGattCallback {
+
+    public abstract void onStartConnect();
+
+    public abstract void onConnectFail(BleDevice bleDevice, BleException exception);
+
+    public abstract void onConnectSuccess(BleDevice bleDevice, BluetoothGatt gatt, int status);
+
+    public abstract void onDisConnected(boolean isActiveDisConnected, BleDevice device, BluetoothGatt gatt, int status);
+
+}

+ 13 - 0
FastBleLib/src/main/java/com/clj/fastble/callback/BleIndicateCallback.java

@@ -0,0 +1,13 @@
+package com.clj.fastble.callback;
+
+
+import com.clj.fastble.exception.BleException;
+
+public abstract class BleIndicateCallback extends BleBaseCallback{
+
+    public abstract void onIndicateSuccess();
+
+    public abstract void onIndicateFailure(BleException exception);
+
+    public abstract void onCharacteristicChanged(byte[] data);
+}

+ 12 - 0
FastBleLib/src/main/java/com/clj/fastble/callback/BleMtuChangedCallback.java

@@ -0,0 +1,12 @@
+package com.clj.fastble.callback;
+
+
+import com.clj.fastble.exception.BleException;
+
+public abstract class BleMtuChangedCallback extends BleBaseCallback {
+
+    public abstract void onSetMTUFailure(BleException exception);
+
+    public abstract void onMtuChanged(int mtu);
+
+}

+ 14 - 0
FastBleLib/src/main/java/com/clj/fastble/callback/BleNotifyCallback.java

@@ -0,0 +1,14 @@
+package com.clj.fastble.callback;
+
+
+import com.clj.fastble.exception.BleException;
+
+public abstract class BleNotifyCallback extends BleBaseCallback {
+
+    public abstract void onNotifySuccess();
+
+    public abstract void onNotifyFailure(BleException exception);
+
+    public abstract void onCharacteristicChanged(byte[] data);
+
+}

+ 12 - 0
FastBleLib/src/main/java/com/clj/fastble/callback/BleReadCallback.java

@@ -0,0 +1,12 @@
+package com.clj.fastble.callback;
+
+
+import com.clj.fastble.exception.BleException;
+
+public abstract class BleReadCallback extends BleBaseCallback {
+
+    public abstract void onReadSuccess(byte[] data);
+
+    public abstract void onReadFailure(BleException exception);
+
+}

+ 12 - 0
FastBleLib/src/main/java/com/clj/fastble/callback/BleRssiCallback.java

@@ -0,0 +1,12 @@
+package com.clj.fastble.callback;
+
+
+import com.clj.fastble.exception.BleException;
+
+public abstract class BleRssiCallback extends BleBaseCallback{
+
+    public abstract void onRssiFailure(BleException exception);
+
+    public abstract void onRssiSuccess(int rssi);
+
+}

+ 13 - 0
FastBleLib/src/main/java/com/clj/fastble/callback/BleScanAndConnectCallback.java

@@ -0,0 +1,13 @@
+package com.clj.fastble.callback;
+
+
+import com.clj.fastble.data.BleDevice;
+
+public abstract class BleScanAndConnectCallback extends BleGattCallback implements BleScanPresenterImp {
+
+    public abstract void onScanFinished(BleDevice scanResult);
+
+    public void onLeScan(BleDevice bleDevice) {
+    }
+
+}

+ 14 - 0
FastBleLib/src/main/java/com/clj/fastble/callback/BleScanCallback.java

@@ -0,0 +1,14 @@
+package com.clj.fastble.callback;
+
+
+import com.clj.fastble.data.BleDevice;
+
+import java.util.List;
+
+public abstract class BleScanCallback implements BleScanPresenterImp {
+
+    public abstract void onScanFinished(List<BleDevice> scanResultList);
+
+    public void onLeScan(BleDevice bleDevice) {
+    }
+}

+ 11 - 0
FastBleLib/src/main/java/com/clj/fastble/callback/BleScanPresenterImp.java

@@ -0,0 +1,11 @@
+package com.clj.fastble.callback;
+
+import com.clj.fastble.data.BleDevice;
+
+public interface BleScanPresenterImp {
+
+    void onScanStarted(boolean success);
+
+    void onScanning(BleDevice bleDevice);
+
+}

+ 12 - 0
FastBleLib/src/main/java/com/clj/fastble/callback/BleWriteCallback.java

@@ -0,0 +1,12 @@
+package com.clj.fastble.callback;
+
+
+import com.clj.fastble.exception.BleException;
+
+public abstract class BleWriteCallback extends BleBaseCallback{
+
+    public abstract void onWriteSuccess(int current, int total, byte[] justWrite);
+
+    public abstract void onWriteFailure(BleException exception);
+
+}

+ 29 - 0
FastBleLib/src/main/java/com/clj/fastble/data/BleConnectStateParameter.java

@@ -0,0 +1,29 @@
+package com.clj.fastble.data;
+
+
+public class BleConnectStateParameter {
+
+    private int status;
+    private boolean isActive;
+
+    public BleConnectStateParameter(int status) {
+        this.status = status;
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public void setStatus(int status) {
+        this.status = status;
+    }
+
+    public boolean isActive() {
+        return isActive;
+    }
+
+    public void setActive(boolean active) {
+        isActive = active;
+    }
+
+}

+ 112 - 0
FastBleLib/src/main/java/com/clj/fastble/data/BleDevice.java

@@ -0,0 +1,112 @@
+package com.clj.fastble.data;
+
+
+import android.bluetooth.BluetoothDevice;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+public class BleDevice implements Parcelable {
+
+    private BluetoothDevice mDevice;
+    private byte[] mScanRecord;
+    private int mRssi;
+    private long mTimestampNanos;
+
+    public BleDevice(BluetoothDevice device) {
+        mDevice = device;
+    }
+
+    public BleDevice(BluetoothDevice device, int rssi, byte[] scanRecord, long timestampNanos) {
+        mDevice = device;
+        mScanRecord = scanRecord;
+        mRssi = rssi;
+        mTimestampNanos = timestampNanos;
+    }
+
+    protected BleDevice(Parcel in) {
+        mDevice = in.readParcelable(BluetoothDevice.class.getClassLoader());
+        mScanRecord = in.createByteArray();
+        mRssi = in.readInt();
+        mTimestampNanos = in.readLong();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mDevice, flags);
+        dest.writeByteArray(mScanRecord);
+        dest.writeInt(mRssi);
+        dest.writeLong(mTimestampNanos);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final Creator<BleDevice> CREATOR = new Creator<BleDevice>() {
+        @Override
+        public BleDevice createFromParcel(Parcel in) {
+            return new BleDevice(in);
+        }
+
+        @Override
+        public BleDevice[] newArray(int size) {
+            return new BleDevice[size];
+        }
+    };
+
+    public String getName() {
+        if (mDevice != null) {
+            return mDevice.getName();
+        }
+        return null;
+    }
+
+    public String getMac() {
+        if (mDevice != null) {
+            return mDevice.getAddress();
+        }
+        return null;
+    }
+
+    public String getKey() {
+        if (mDevice != null) {
+            return mDevice.getName() + mDevice.getAddress();
+        }
+        return "";
+    }
+
+    public BluetoothDevice getDevice() {
+        return mDevice;
+    }
+
+    public void setDevice(BluetoothDevice device) {
+        this.mDevice = device;
+    }
+
+    public byte[] getScanRecord() {
+        return mScanRecord;
+    }
+
+    public void setScanRecord(byte[] scanRecord) {
+        this.mScanRecord = scanRecord;
+    }
+
+    public int getRssi() {
+        return mRssi;
+    }
+
+    public void setRssi(int rssi) {
+        this.mRssi = rssi;
+    }
+
+    public long getTimestampNanos() {
+        return mTimestampNanos;
+    }
+
+    public void setTimestampNanos(long timestampNanos) {
+        this.mTimestampNanos = timestampNanos;
+    }
+
+}

+ 60 - 0
FastBleLib/src/main/java/com/clj/fastble/data/BleMsg.java

@@ -0,0 +1,60 @@
+package com.clj.fastble.data;
+
+
+
+public class BleMsg {
+
+    // Scan
+    public static final int MSG_SCAN_DEVICE = 0X00;
+
+    // Connect
+    public static final int MSG_CONNECT_FAIL = 0x01;
+    public static final int MSG_DISCONNECTED = 0x02;
+    public static final int MSG_RECONNECT = 0x03;
+    public static final int MSG_DISCOVER_SERVICES = 0x04;
+    public static final int MSG_DISCOVER_FAIL = 0x05;
+    public static final int MSG_DISCOVER_SUCCESS = 0x06;
+    public static final int MSG_CONNECT_OVER_TIME = 0x07;
+
+    // Notify
+    public static final int MSG_CHA_NOTIFY_START = 0x11;
+    public static final int MSG_CHA_NOTIFY_RESULT = 0x12;
+    public static final int MSG_CHA_NOTIFY_DATA_CHANGE = 0x13;
+    public static final String KEY_NOTIFY_BUNDLE_STATUS = "notify_status";
+    public static final String KEY_NOTIFY_BUNDLE_VALUE = "notify_value";
+
+    // Indicate
+    public static final int MSG_CHA_INDICATE_START = 0x21;
+    public static final int MSG_CHA_INDICATE_RESULT = 0x22;
+    public static final int MSG_CHA_INDICATE_DATA_CHANGE = 0x23;
+    public static final String KEY_INDICATE_BUNDLE_STATUS = "indicate_status";
+    public static final String KEY_INDICATE_BUNDLE_VALUE = "indicate_value";
+
+    // Write
+    public static final int MSG_CHA_WRITE_START = 0x31;
+    public static final int MSG_CHA_WRITE_RESULT = 0x32;
+    public static final int MSG_SPLIT_WRITE_NEXT = 0x33;
+    public static final String KEY_WRITE_BUNDLE_STATUS = "write_status";
+    public static final String KEY_WRITE_BUNDLE_VALUE = "write_value";
+
+    // Read
+    public static final int MSG_CHA_READ_START = 0x41;
+    public static final int MSG_CHA_READ_RESULT = 0x42;
+    public static final String KEY_READ_BUNDLE_STATUS = "read_status";
+    public static final String KEY_READ_BUNDLE_VALUE = "read_value";
+
+    // Rssi
+    public static final int MSG_READ_RSSI_START = 0x51;
+    public static final int MSG_READ_RSSI_RESULT = 0x52;
+    public static final String KEY_READ_RSSI_BUNDLE_STATUS = "rssi_status";
+    public static final String KEY_READ_RSSI_BUNDLE_VALUE = "rssi_value";
+
+    // Mtu
+    public static final int MSG_SET_MTU_START = 0x61;
+    public static final int MSG_SET_MTU_RESULT = 0x62;
+    public static final String KEY_SET_MTU_BUNDLE_STATUS = "mtu_status";
+    public static final String KEY_SET_MTU_BUNDLE_VALUE = "mtu_value";
+
+
+
+}

+ 18 - 0
FastBleLib/src/main/java/com/clj/fastble/data/BleScanState.java

@@ -0,0 +1,18 @@
+package com.clj.fastble.data;
+
+
+public enum BleScanState {
+
+    STATE_IDLE(-1),
+    STATE_SCANNING(0X01);
+
+    private int code;
+
+    BleScanState(int code) {
+        this.code = code;
+    }
+
+    public int getCode() {
+        return code;
+    }
+}

+ 8 - 0
FastBleLib/src/main/java/com/clj/fastble/data/BleWriteState.java

@@ -0,0 +1,8 @@
+package com.clj.fastble.data;
+
+
+
+public class BleWriteState {
+
+    public static final int DATA_WRITE_SINGLE = 1;
+}

+ 47 - 0
FastBleLib/src/main/java/com/clj/fastble/exception/BleException.java

@@ -0,0 +1,47 @@
+package com.clj.fastble.exception;
+
+import java.io.Serializable;
+
+
+public abstract class BleException implements Serializable {
+
+    private static final long serialVersionUID = 8004414918500865564L;
+
+    public static final int ERROR_CODE_TIMEOUT = 100;
+    public static final int ERROR_CODE_GATT = 101;
+    public static final int ERROR_CODE_OTHER = 102;
+
+    private int code;
+    private String description;
+
+    public BleException(int code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public BleException setCode(int code) {
+        this.code = code;
+        return this;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public BleException setDescription(String description) {
+        this.description = description;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "BleException { " +
+               "code=" + code +
+               ", description='" + description + '\'' +
+               '}';
+    }
+}

+ 42 - 0
FastBleLib/src/main/java/com/clj/fastble/exception/ConnectException.java

@@ -0,0 +1,42 @@
+package com.clj.fastble.exception;
+
+import android.bluetooth.BluetoothGatt;
+
+
+public class ConnectException extends BleException {
+
+    private BluetoothGatt bluetoothGatt;
+    private int gattStatus;
+
+    public ConnectException(BluetoothGatt bluetoothGatt, int gattStatus) {
+        super(ERROR_CODE_GATT, "Gatt Exception Occurred! ");
+        this.bluetoothGatt = bluetoothGatt;
+        this.gattStatus = gattStatus;
+    }
+
+    public int getGattStatus() {
+        return gattStatus;
+    }
+
+    public ConnectException setGattStatus(int gattStatus) {
+        this.gattStatus = gattStatus;
+        return this;
+    }
+
+    public BluetoothGatt getBluetoothGatt() {
+        return bluetoothGatt;
+    }
+
+    public ConnectException setBluetoothGatt(BluetoothGatt bluetoothGatt) {
+        this.bluetoothGatt = bluetoothGatt;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "ConnectException{" +
+               "gattStatus=" + gattStatus +
+               ", bluetoothGatt=" + bluetoothGatt +
+               "} " + super.toString();
+    }
+}

+ 28 - 0
FastBleLib/src/main/java/com/clj/fastble/exception/GattException.java

@@ -0,0 +1,28 @@
+package com.clj.fastble.exception;
+
+
+public class GattException extends BleException {
+
+    private int gattStatus;
+
+    public GattException(int gattStatus) {
+        super(ERROR_CODE_GATT, "Gatt Exception Occurred! ");
+        this.gattStatus = gattStatus;
+    }
+
+    public int getGattStatus() {
+        return gattStatus;
+    }
+
+    public GattException setGattStatus(int gattStatus) {
+        this.gattStatus = gattStatus;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        return "GattException{" +
+               "gattStatus=" + gattStatus +
+               "} " + super.toString();
+    }
+}

+ 10 - 0
FastBleLib/src/main/java/com/clj/fastble/exception/OtherException.java

@@ -0,0 +1,10 @@
+package com.clj.fastble.exception;
+
+
+public class OtherException extends BleException {
+
+    public OtherException(String description) {
+        super(ERROR_CODE_OTHER, description);
+    }
+
+}

+ 10 - 0
FastBleLib/src/main/java/com/clj/fastble/exception/TimeoutException.java

@@ -0,0 +1,10 @@
+package com.clj.fastble.exception;
+
+
+public class TimeoutException extends BleException {
+
+    public TimeoutException() {
+        super(ERROR_CODE_TIMEOUT, "Timeout Exception Occurred!");
+    }
+
+}

+ 238 - 0
FastBleLib/src/main/java/com/clj/fastble/scan/BleScanPresenter.java

@@ -0,0 +1,238 @@
+package com.clj.fastble.scan;
+
+
+import android.annotation.TargetApi;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.os.Build;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.text.TextUtils;
+
+import com.clj.fastble.callback.BleScanPresenterImp;
+import com.clj.fastble.data.BleDevice;
+import com.clj.fastble.data.BleMsg;
+import com.clj.fastble.utils.BleLog;
+import com.clj.fastble.utils.HexUtil;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public abstract class BleScanPresenter implements BluetoothAdapter.LeScanCallback {
+
+    private String[] mDeviceNames;
+    private String mDeviceMac;
+    private boolean mFuzzy;
+    private boolean mCanRepeatFound;
+    private boolean mNeedConnect;
+    private long mScanTimeout;
+    private BleScanPresenterImp mBleScanPresenterImp;
+
+    private final List<BleDevice> mBleDeviceList = new ArrayList<>();
+
+    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private boolean mHandling;
+
+    private static final class ScanHandler extends Handler {
+
+        private final WeakReference<BleScanPresenter> mBleScanPresenter;
+
+        ScanHandler(Looper looper, BleScanPresenter bleScanPresenter) {
+            super(looper);
+            mBleScanPresenter = new WeakReference<>(bleScanPresenter);
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            BleScanPresenter bleScanPresenter = mBleScanPresenter.get();
+            if (bleScanPresenter != null) {
+                if (msg.what == BleMsg.MSG_SCAN_DEVICE) {
+                    final BleDevice bleDevice = (BleDevice) msg.obj;
+                    if (bleDevice != null) {
+                        bleScanPresenter.handleResult(bleDevice);
+//                        BleLog.e("bleDevice:"+bleDevice.getMac());
+                    }
+                }
+            }
+        }
+    }
+
+    private void handleResult(final BleDevice bleDevice) {
+        mMainHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                onLeScan(bleDevice);
+            }
+        });
+        checkDevice(bleDevice);
+    }
+
+    public void prepare(String[] names, String mac, boolean fuzzy, boolean needConnect,
+                        long timeOut, BleScanPresenterImp bleScanPresenterImp, boolean isCanRepeatFound) {
+        mDeviceNames = names;
+        mDeviceMac = mac;
+        mFuzzy = fuzzy;
+        mNeedConnect = needConnect;
+        mScanTimeout = timeOut;
+        mBleScanPresenterImp = bleScanPresenterImp;
+        mCanRepeatFound = isCanRepeatFound;
+
+        mHandlerThread = new HandlerThread(BleScanPresenter.class.getSimpleName());
+        mHandlerThread.start();
+        mHandler = new ScanHandler(mHandlerThread.getLooper(), this);
+        mHandling = true;
+    }
+
+    public boolean ismNeedConnect() {
+        return mNeedConnect;
+    }
+
+    public BleScanPresenterImp getBleScanPresenterImp() {
+        return mBleScanPresenterImp;
+    }
+
+    @Override
+    public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
+        if (device == null)
+            return;
+
+        if (!mHandling)
+            return;
+
+        Message message = mHandler.obtainMessage();
+        message.what = BleMsg.MSG_SCAN_DEVICE;
+        message.obj = new BleDevice(device, rssi, scanRecord, System.currentTimeMillis());
+        mHandler.sendMessage(message);
+    }
+
+    private void checkDevice(BleDevice bleDevice) {
+        if (TextUtils.isEmpty(mDeviceMac) && (mDeviceNames == null || mDeviceNames.length < 1)) {
+            correctDeviceAndNextStep(bleDevice);
+            return;
+        }
+
+        if (!TextUtils.isEmpty(mDeviceMac)) {
+            if (!mDeviceMac.equalsIgnoreCase(bleDevice.getMac()))
+                return;
+        }
+
+        if (mDeviceNames != null && mDeviceNames.length > 0) {
+            AtomicBoolean equal = new AtomicBoolean(false);
+            for (String name : mDeviceNames) {
+                String remoteName = bleDevice.getName();
+                if (remoteName == null)
+                    remoteName = "";
+                if (mFuzzy ? remoteName.contains(name) : remoteName.equals(name)) {
+                    equal.set(true);
+                }
+            }
+            if (!equal.get()) {
+                return;
+            }
+        }
+
+        correctDeviceAndNextStep(bleDevice);
+    }
+
+
+    private void correctDeviceAndNextStep(final BleDevice bleDevice) {
+        if (mNeedConnect) {
+            BleLog.i("devices detected  ------"
+                    + "  name:" + bleDevice.getName()
+                    + "  mac:" + bleDevice.getMac()
+                    + "  Rssi:" + bleDevice.getRssi()
+                    + "  scanRecord:" + HexUtil.formatHexString(bleDevice.getScanRecord()));
+
+            mBleDeviceList.add(bleDevice);
+            mMainHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    BleScanner.getInstance().stopLeScan();
+                }
+            });
+
+        } else {
+            AtomicBoolean hasFound = new AtomicBoolean(false);
+            for (BleDevice result : mBleDeviceList) {
+                if (result.getDevice().equals(bleDevice.getDevice())) {
+                    hasFound.set(true);
+                }
+            }
+            if (!hasFound.get() || mCanRepeatFound) {
+                BleLog.i(mCanRepeatFound + " device detected  ------"
+                        + "  name: " + bleDevice.getName()
+                        + "  mac: " + bleDevice.getMac()
+                        + "  Rssi: " + bleDevice.getRssi()
+                        + "  scanRecord: " + HexUtil.formatHexString(bleDevice.getScanRecord(), true));
+
+                if (!hasFound.get()) {
+                    mBleDeviceList.add(bleDevice);
+                }
+                mMainHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        onScanning(bleDevice);
+                    }
+                });
+            }
+        }
+    }
+
+    public final void notifyScanStarted(final boolean success) {
+        mBleDeviceList.clear();
+
+        removeHandlerMsg();
+
+        if (success && mScanTimeout > 0) {
+            mMainHandler.postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    BleScanner.getInstance().stopLeScan();
+                }
+            }, mScanTimeout);
+        }
+
+        mMainHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                onScanStarted(success);
+            }
+        });
+    }
+
+    public final void notifyScanStopped() {
+        mHandling = false;
+        if (mHandlerThread == null){
+            return;
+        }
+
+        mHandlerThread.quit();
+        removeHandlerMsg();
+        mMainHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                onScanFinished(mBleDeviceList);
+            }
+        });
+    }
+
+    public final void removeHandlerMsg() {
+        mMainHandler.removeCallbacksAndMessages(null);
+        mHandler.removeCallbacksAndMessages(null);
+    }
+
+    public abstract void onScanStarted(boolean success);
+
+    public abstract void onLeScan(BleDevice bleDevice);
+
+    public abstract void onScanning(BleDevice bleDevice);
+
+    public abstract void onScanFinished(List<BleDevice> bleDeviceList);
+}

+ 109 - 0
FastBleLib/src/main/java/com/clj/fastble/scan/BleScanRuleConfig.java

@@ -0,0 +1,109 @@
+package com.clj.fastble.scan;
+
+
+import com.clj.fastble.BleManager;
+
+import java.util.UUID;
+
+public class BleScanRuleConfig {
+
+    private UUID[] mServiceUuids = null;
+    private String[] mDeviceNames = null;
+    private String mDeviceMac = null;
+    private boolean mAutoConnect = false;
+    private boolean mCanRepeatFound = false;
+    private boolean mFuzzy = false;
+    private long mScanTimeOut = BleManager.DEFAULT_SCAN_TIME;
+
+    public UUID[] getServiceUuids() {
+        return mServiceUuids;
+    }
+
+    public String[] getDeviceNames() {
+        return mDeviceNames;
+    }
+
+    public String getDeviceMac() {
+        return mDeviceMac;
+    }
+
+    public boolean isAutoConnect() {
+        return mAutoConnect;
+    }
+
+    public boolean isFuzzy() {
+        return mFuzzy;
+    }
+
+    public boolean isCanRepeatFound() {
+        return mCanRepeatFound;
+    }
+
+    public long getScanTimeOut() {
+        return mScanTimeOut;
+    }
+
+    public static class Builder {
+
+        private UUID[] mServiceUuids = null;
+        private String[] mDeviceNames = null;
+        private String mDeviceMac = null;
+        private boolean mAutoConnect = false;
+        private boolean mCanRepeatFound = false;
+        private boolean mFuzzy = false;
+        private long mTimeOut = BleManager.DEFAULT_SCAN_TIME;
+
+        public Builder setServiceUuids(UUID[] uuids) {
+            this.mServiceUuids = uuids;
+            return this;
+        }
+
+        public Builder setDeviceName(boolean fuzzy, String... name) {
+            this.mFuzzy = fuzzy;
+            this.mDeviceNames = name;
+            return this;
+        }
+
+        public Builder setDeviceMac(String mac) {
+            this.mDeviceMac = mac;
+            return this;
+        }
+
+        public Builder setAutoConnect(boolean autoConnect) {
+            this.mAutoConnect = autoConnect;
+            return this;
+        }
+
+        /**
+         * 可以重复发现设备
+         */
+        public Builder setCanRepeatFound(boolean isCanRepeat) {
+            this.mCanRepeatFound = isCanRepeat;
+            return this;
+        }
+
+        public Builder setScanTimeOut(long timeOut) {
+            this.mTimeOut = timeOut;
+            return this;
+        }
+
+        void applyConfig(BleScanRuleConfig config) {
+            config.mServiceUuids = this.mServiceUuids;
+            config.mDeviceNames = this.mDeviceNames;
+            config.mDeviceMac = this.mDeviceMac;
+            config.mAutoConnect = this.mAutoConnect;
+            config.mFuzzy = this.mFuzzy;
+            config.mScanTimeOut = this.mTimeOut;
+            config.mCanRepeatFound = this.mCanRepeatFound;
+        }
+
+        public BleScanRuleConfig build() {
+            BleScanRuleConfig config = new BleScanRuleConfig();
+            applyConfig(config);
+            return config;
+        }
+
+    }
+
+
+}

+ 139 - 0
FastBleLib/src/main/java/com/clj/fastble/scan/BleScanner.java

@@ -0,0 +1,139 @@
+package com.clj.fastble.scan;
+
+
+import android.annotation.TargetApi;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+
+import com.clj.fastble.BleManager;
+import com.clj.fastble.callback.BleScanAndConnectCallback;
+import com.clj.fastble.callback.BleScanCallback;
+import com.clj.fastble.callback.BleScanPresenterImp;
+import com.clj.fastble.data.BleDevice;
+import com.clj.fastble.data.BleScanState;
+import com.clj.fastble.utils.BleLog;
+
+import java.util.List;
+import java.util.UUID;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
+public class BleScanner {
+
+    public static BleScanner getInstance() {
+        return BleScannerHolder.sBleScanner;
+    }
+
+    private static class BleScannerHolder {
+        private static final BleScanner sBleScanner = new BleScanner();
+    }
+
+    private BleScanState mBleScanState = BleScanState.STATE_IDLE;
+
+    private final BleScanPresenter mBleScanPresenter = new BleScanPresenter() {
+
+        @Override
+        public void onScanStarted(boolean success) {
+            BleScanPresenterImp callback = mBleScanPresenter.getBleScanPresenterImp();
+            if (callback != null) {
+                callback.onScanStarted(success);
+            }
+        }
+
+        @Override
+        public void onLeScan(BleDevice bleDevice) {
+            if (mBleScanPresenter.ismNeedConnect()) {
+                BleScanAndConnectCallback callback = (BleScanAndConnectCallback)
+                        mBleScanPresenter.getBleScanPresenterImp();
+                if (callback != null) {
+                    callback.onLeScan(bleDevice);
+                }
+            } else {
+                BleScanCallback callback = (BleScanCallback) mBleScanPresenter.getBleScanPresenterImp();
+                if (callback != null) {
+                    callback.onLeScan(bleDevice);
+                }
+            }
+        }
+
+        @Override
+        public void onScanning(BleDevice result) {
+            BleScanPresenterImp callback = mBleScanPresenter.getBleScanPresenterImp();
+            if (callback != null) {
+                callback.onScanning(result);
+            }
+        }
+
+        @Override
+        public void onScanFinished(List<BleDevice> bleDeviceList) {
+            if (mBleScanPresenter.ismNeedConnect()) {
+                final BleScanAndConnectCallback callback = (BleScanAndConnectCallback)
+                        mBleScanPresenter.getBleScanPresenterImp();
+                if (bleDeviceList == null || bleDeviceList.size() < 1) {
+                    if (callback != null) {
+                        callback.onScanFinished(null);
+                    }
+                } else {
+                    if (callback != null) {
+                        callback.onScanFinished(bleDeviceList.get(0));
+                    }
+                    final List<BleDevice> list = bleDeviceList;
+                    new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
+                        @Override
+                        public void run() {
+                            BleManager.getInstance().connect(list.get(0), callback);
+                        }
+                    }, 100);
+                }
+            } else {
+                BleScanCallback callback = (BleScanCallback) mBleScanPresenter.getBleScanPresenterImp();
+                if (callback != null) {
+                    callback.onScanFinished(bleDeviceList);
+                }
+            }
+        }
+    };
+
+    public void scan(UUID[] serviceUuids, String[] names, String mac, boolean fuzzy,
+                     long timeOut, final BleScanCallback callback,boolean isCanRepeatFound) {
+
+        startLeScan(serviceUuids, names, mac, fuzzy, false, timeOut, callback,isCanRepeatFound);
+    }
+
+    public void scanAndConnect(UUID[] serviceUuids, String[] names, String mac, boolean fuzzy,
+                               long timeOut, BleScanAndConnectCallback callback) {
+
+        startLeScan(serviceUuids, names, mac, fuzzy, true, timeOut, callback,false);
+    }
+
+    private synchronized void startLeScan(UUID[] serviceUuids, String[] names, String mac, boolean fuzzy,
+                                          boolean needConnect, long timeOut, BleScanPresenterImp imp,boolean isCanRepeatFound) {
+
+        if (mBleScanState != BleScanState.STATE_IDLE) {
+            BleLog.w("scan action already exists, complete the previous scan action first");
+            if (imp != null) {
+                imp.onScanStarted(false);
+            }
+            return;
+        }
+
+        mBleScanPresenter.prepare(names, mac, fuzzy, needConnect, timeOut, imp,isCanRepeatFound);
+
+        boolean success = BleManager.getInstance().getBluetoothAdapter()
+                .startLeScan(serviceUuids, mBleScanPresenter);
+        mBleScanState = success ? BleScanState.STATE_SCANNING : BleScanState.STATE_IDLE;
+        mBleScanPresenter.notifyScanStarted(success);
+    }
+
+    public synchronized void stopLeScan() {
+        BleManager.getInstance().getBluetoothAdapter().stopLeScan(mBleScanPresenter);
+        mBleScanState = BleScanState.STATE_IDLE;
+        mBleScanPresenter.notifyScanStopped();
+    }
+
+    public BleScanState getScanState() {
+        return mBleScanState;
+    }
+
+
+}

+ 31 - 0
FastBleLib/src/main/java/com/clj/fastble/utils/BleLog.java

@@ -0,0 +1,31 @@
+package com.clj.fastble.utils;
+
+
+import android.util.Log;
+
+public final class BleLog {
+
+    public static boolean isPrint = true;
+    private static final String defaultTag = "FastBle";
+
+    public static void d(String msg) {
+        if (isPrint && msg != null)
+            Log.d(defaultTag, msg);
+    }
+
+    public static void i(String msg) {
+        if (isPrint && msg != null)
+            Log.i(defaultTag, msg);
+    }
+
+    public static void w(String msg) {
+        if (isPrint && msg != null)
+            Log.w(defaultTag, msg);
+    }
+
+    public static void e(String msg) {
+        if (isPrint && msg != null)
+            Log.e(defaultTag, msg);
+    }
+
+}

+ 34 - 0
FastBleLib/src/main/java/com/clj/fastble/utils/BleLruHashMap.java

@@ -0,0 +1,34 @@
+package com.clj.fastble.utils;
+
+
+import com.clj.fastble.bluetooth.BleBluetooth;
+
+import java.util.LinkedHashMap;
+
+public class BleLruHashMap<K, V> extends LinkedHashMap<K, V> {
+
+    private final int MAX_SIZE;
+
+    public BleLruHashMap(int saveSize) {
+        super((int) Math.ceil(saveSize / 0.75) + 1, 0.75f, true);
+        MAX_SIZE = saveSize;
+    }
+
+    @Override
+    protected boolean removeEldestEntry(java.util.Map.Entry eldest) {
+        if (size() > MAX_SIZE && eldest.getValue() instanceof BleBluetooth) {
+            ((BleBluetooth) eldest.getValue()).disconnect();
+        }
+        return size() > MAX_SIZE;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        for (Entry<K, V> entry : entrySet()) {
+            sb.append(String.format("%s:%s ", entry.getKey(), entry.getValue()));
+        }
+        return sb.toString();
+    }
+
+}

+ 121 - 0
FastBleLib/src/main/java/com/clj/fastble/utils/HexUtil.java

@@ -0,0 +1,121 @@
+package com.clj.fastble.utils;
+
+public class HexUtil {
+
+    private static final char[] DIGITS_LOWER = {'0', '1', '2', '3', '4', '5',
+            '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+
+    private static final char[] DIGITS_UPPER = {'0', '1', '2', '3', '4', '5',
+            '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
+
+    public static char[] encodeHex(byte[] data) {
+        return encodeHex(data, true);
+    }
+
+    public static char[] encodeHex(byte[] data, boolean toLowerCase) {
+        return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
+    }
+
+    protected static char[] encodeHex(byte[] data, char[] toDigits) {
+        if (data == null)
+            return null;
+        int l = data.length;
+        char[] out = new char[l << 1];
+        for (int i = 0, j = 0; i < l; i++) {
+            out[j++] = toDigits[(0xF0 & data[i]) >>> 4];
+            out[j++] = toDigits[0x0F & data[i]];
+        }
+        return out;
+    }
+
+
+    public static String encodeHexStr(byte[] data) {
+        return encodeHexStr(data, true);
+    }
+
+    public static String encodeHexStr(byte[] data, boolean toLowerCase) {
+        return encodeHexStr(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER);
+    }
+
+
+    protected static String encodeHexStr(byte[] data, char[] toDigits) {
+        return new String(encodeHex(data, toDigits));
+    }
+
+    public static String formatHexString(byte[] data) {
+        return formatHexString(data, false);
+    }
+
+    public static String formatHexString(byte[] data, boolean addSpace) {
+        if (data == null || data.length < 1)
+            return null;
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < data.length; i++) {
+            String hex = Integer.toHexString(data[i] & 0xFF);
+            if (hex.length() == 1) {
+                hex = '0' + hex;
+            }
+            sb.append(hex);
+            if (addSpace)
+                sb.append(" ");
+        }
+        return sb.toString().trim();
+    }
+
+    public static byte[] decodeHex(char[] data) {
+        int len = data.length;
+
+        if ((len & 0x01) != 0) {
+            throw new RuntimeException("Odd number of characters.");
+        }
+
+        byte[] out = new byte[len >> 1];
+
+        // two characters form the hex value.
+        for (int i = 0, j = 0; j < len; i++) {
+            int f = toDigit(data[j], j) << 4;
+            j++;
+            f = f | toDigit(data[j], j);
+            j++;
+            out[i] = (byte) (f & 0xFF);
+        }
+
+        return out;
+    }
+
+
+    protected static int toDigit(char ch, int index) {
+        int digit = Character.digit(ch, 16);
+        if (digit == -1) {
+            throw new RuntimeException("Illegal hexadecimal character " + ch
+                    + " at index " + index);
+        }
+        return digit;
+    }
+
+
+    public static byte[] hexStringToBytes(String hexString) {
+        if (hexString == null || hexString.equals("")) {
+            return null;
+        }
+        hexString = hexString.trim();
+        hexString = hexString.toUpperCase();
+        int length = hexString.length() / 2;
+        char[] hexChars = hexString.toCharArray();
+        byte[] d = new byte[length];
+        for (int i = 0; i < length; i++) {
+            int pos = i * 2;
+            d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1]));
+        }
+        return d;
+    }
+
+    public static byte charToByte(char c) {
+        return (byte) "0123456789ABCDEF".indexOf(c);
+    }
+
+    public static String extractData(byte[] data, int position) {
+        return HexUtil.formatHexString(new byte[]{data[position]});
+    }
+
+}

+ 1 - 0
RangeSeekBar/.gitignore

@@ -0,0 +1 @@
+/build

+ 39 - 0
RangeSeekBar/build.gradle

@@ -0,0 +1,39 @@
+apply plugin: 'com.android.library'
+group='com.github.Jay-Goo'
+
+android {
+    compileSdkVersion 28
+
+
+    defaultConfig {
+        minSdkVersion 16
+        targetSdkVersion 28
+        versionCode 7
+        versionName "3.0.0"
+
+        testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+
+    implementation "androidx.annotation:annotation:1.1.0"
+    implementation 'androidx.appcompat:appcompat:1.5.1'
+
+
+}
+
+// 指定编码
+tasks.withType(JavaCompile) {
+    options.encoding = "UTF-8"
+}
+

+ 17 - 0
RangeSeekBar/proguard-rules.pro

@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/mac/Library/Android/sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}

+ 26 - 0
RangeSeekBar/src/androidTest/java/com/jaygoo/widget/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package com.jaygoo.widget;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() throws Exception {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getTargetContext();
+
+        assertEquals("com.jaygoo.widget.test", appContext.getPackageName());
+    }
+}

+ 11 - 0
RangeSeekBar/src/main/AndroidManifest.xml

@@ -0,0 +1,11 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.jaygoo.widget">
+
+    <application android:allowBackup="true"
+                 android:label="@string/app_name"
+                 android:supportsRtl="true"
+    >
+
+    </application>
+
+</manifest>

+ 17 - 0
RangeSeekBar/src/main/java/com/jaygoo/widget/OnRangeChangedListener.java

@@ -0,0 +1,17 @@
+package com.jaygoo.widget;
+
+/**
+ * ================================================
+ * 作    者:JayGoo
+ * 版    本:
+ * 创建日期:2018/5/8
+ * 描    述:
+ * ================================================
+ */
+public interface OnRangeChangedListener {
+    void onRangeChanged(RangeSeekBar view, float leftValue, float rightValue, boolean isFromUser);
+
+    void onStartTrackingTouch(RangeSeekBar view, boolean isLeft);
+
+    void onStopTrackingTouch(RangeSeekBar view, boolean isLeft);
+}

+ 1241 - 0
RangeSeekBar/src/main/java/com/jaygoo/widget/RangeSeekBar.java

@@ -0,0 +1,1241 @@
+package com.jaygoo.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.os.Parcelable;
+
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.List;
+
+import static com.jaygoo.widget.SeekBar.INDICATOR_ALWAYS_HIDE;
+import static com.jaygoo.widget.SeekBar.INDICATOR_ALWAYS_SHOW;
+
+
+public class RangeSeekBar extends View {
+
+    private final static int MIN_INTERCEPT_DISTANCE = 100;
+
+    //normal seekBar mode
+    public final static int SEEKBAR_MODE_SINGLE = 1;
+    //RangeSeekBar
+    public final static int SEEKBAR_MODE_RANGE = 2;
+
+    /**
+     * @hide
+     */
+    @IntDef({SEEKBAR_MODE_SINGLE, SEEKBAR_MODE_RANGE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SeekBarModeDef {
+    }
+
+    //number according to the actual proportion of the number of arranged;
+    public final static int TRICK_MARK_MODE_NUMBER = 0;
+    //other equally arranged
+    public final static int TRICK_MARK_MODE_OTHER = 1;
+
+    /**
+     * @hide
+     */
+    @IntDef({TRICK_MARK_MODE_NUMBER, TRICK_MARK_MODE_OTHER})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TickMarkModeDef {
+    }
+
+    //tick mark text gravity
+    public final static int TICK_MARK_GRAVITY_LEFT = 0;
+    public final static int TICK_MARK_GRAVITY_CENTER = 1;
+    public final static int TICK_MARK_GRAVITY_RIGHT = 2;
+
+    /**
+     * @hide
+     */
+    @IntDef({TICK_MARK_GRAVITY_LEFT, TICK_MARK_GRAVITY_CENTER, TICK_MARK_GRAVITY_RIGHT})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TickMarkGravityDef {
+    }
+
+    /**
+     * @hide
+     */
+    @IntDef({Gravity.TOP, Gravity.BOTTOM})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TickMarkLayoutGravityDef {
+    }
+
+    /**
+     * @hide
+     */
+    @IntDef({Gravity.TOP, Gravity.CENTER, Gravity.BOTTOM})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface GravityDef {
+    }
+
+    public static class Gravity {
+        public final static int TOP = 0;
+        public final static int BOTTOM = 1;
+        public final static int CENTER = 2;
+    }
+
+    private int progressTop, progressBottom, progressLeft, progressRight;
+    private int seekBarMode;
+    //刻度模式:number根据数字实际比例排列;other 均分排列
+    private int tickMarkMode;
+    //刻度与进度条间的间距
+    //The spacing between the tick mark and the progress bar
+    private int tickMarkTextMargin;
+    //刻度文字与提示文字的大小
+    //tick mark text and prompt text size
+    private int tickMarkTextSize;
+    private int tickMarkGravity;
+    private int tickMarkLayoutGravity;
+    private int tickMarkTextColor;
+    private int tickMarkInRangeTextColor;
+    //刻度上显示的文字
+    //The texts displayed on the scale
+    private CharSequence[] tickMarkTextArray;
+    //进度条圆角
+    //radius of progress bar
+    private float progressRadius;
+    //进度中进度条的颜色
+    //the color of seekBar in progress
+    private int progressColor;
+    //默认进度条颜色
+    //the default color of the progress bar
+    private int progressDefaultColor;
+
+    //the drawable of seekBar in progress
+    private int progressDrawableId;
+    //the default Drawable of the progress bar
+    private int progressDefaultDrawableId;
+
+    //the progress height
+    private int progressHeight;
+    // the progress width
+    private int progressWidth;
+    //the range interval of RangeSeekBar
+    private float minInterval;
+
+    private int gravity;
+    //enable RangeSeekBar two thumb Overlap
+    private boolean enableThumbOverlap;
+
+    //the color of step divs
+    private int stepsColor;
+    //the width of each step
+    private float stepsWidth;
+    //the height of each step
+    private float stepsHeight;
+    //the radius of step divs
+    private float stepsRadius;
+    //steps is 0 will disable StepSeekBar
+    private int steps;
+    //the thumb will automatic bonding close to its value
+    private boolean stepsAutoBonding;
+    private int stepsDrawableId;
+    //True values set by the user
+    private float minProgress, maxProgress;
+    //****************** the above is attr value  ******************//
+
+    private boolean isEnable = true;
+    float touchDownX,touchDownY;
+    //剩余最小间隔的进度
+    float reservePercent;
+    boolean isScaleThumb = false;
+    Paint paint = new Paint();
+    RectF progressDefaultDstRect = new RectF();
+    RectF progressDstRect = new RectF();
+    Rect progressSrcRect = new Rect();
+    RectF stepDivRect = new RectF();
+    Rect tickMarkTextRect = new Rect();
+    SeekBar leftSB;
+    SeekBar rightSB;
+    SeekBar currTouchSB;
+    Bitmap progressBitmap;
+    Bitmap progressDefaultBitmap;
+    List<Bitmap> stepsBitmaps = new ArrayList<>();
+    private int progressPaddingRight;
+    private OnRangeChangedListener callback;
+
+    public RangeSeekBar(Context context) {
+        this(context, null);
+    }
+
+    public RangeSeekBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initAttrs(attrs);
+        initPaint();
+        initSeekBar(attrs);
+        initStepsBitmap();
+    }
+
+    private void initProgressBitmap() {
+        if (progressBitmap == null) {
+            progressBitmap = Utils.drawableToBitmap(getContext(), progressWidth, progressHeight, progressDrawableId);
+        }
+        if (progressDefaultBitmap == null) {
+            progressDefaultBitmap = Utils.drawableToBitmap(getContext(), progressWidth, progressHeight, progressDefaultDrawableId);
+        }
+    }
+
+    private boolean verifyStepsMode() {
+        if (steps < 1 || stepsHeight <= 0 || stepsWidth <= 0) return false;
+        return true;
+    }
+
+    private void initStepsBitmap() {
+        if (!verifyStepsMode() || stepsDrawableId == 0) return;
+        if (stepsBitmaps.isEmpty()) {
+            Bitmap bitmap = Utils.drawableToBitmap(getContext(), (int) stepsWidth, (int) stepsHeight, stepsDrawableId);
+            for (int i = 0; i <= steps; i++) {
+                stepsBitmaps.add(bitmap);
+            }
+        }
+    }
+
+    private void initSeekBar(AttributeSet attrs) {
+        leftSB = new SeekBar(this, attrs, true);
+        rightSB = new SeekBar(this, attrs, false);
+        rightSB.setVisible(seekBarMode != SEEKBAR_MODE_SINGLE);
+    }
+
+
+    private void initAttrs(AttributeSet attrs) {
+        try {
+            TypedArray t = getContext().obtainStyledAttributes(attrs, R.styleable.RangeSeekBar);
+            seekBarMode = t.getInt(R.styleable.RangeSeekBar_rsb_mode, SEEKBAR_MODE_RANGE);
+            minProgress = t.getFloat(R.styleable.RangeSeekBar_rsb_min, 0);
+            maxProgress = t.getFloat(R.styleable.RangeSeekBar_rsb_max, 100);
+            minInterval = t.getFloat(R.styleable.RangeSeekBar_rsb_min_interval, 0);
+            gravity = t.getInt(R.styleable.RangeSeekBar_rsb_gravity, Gravity.TOP);
+            progressColor = t.getColor(R.styleable.RangeSeekBar_rsb_progress_color, 0xFF4BD962);
+            progressRadius = (int) t.getDimension(R.styleable.RangeSeekBar_rsb_progress_radius, -1);
+            progressDefaultColor = t.getColor(R.styleable.RangeSeekBar_rsb_progress_default_color, 0xFFD7D7D7);
+            progressDrawableId = t.getResourceId(R.styleable.RangeSeekBar_rsb_progress_drawable, 0);
+            progressDefaultDrawableId = t.getResourceId(R.styleable.RangeSeekBar_rsb_progress_drawable_default, 0);
+            progressHeight = (int) t.getDimension(R.styleable.RangeSeekBar_rsb_progress_height, Utils.dp2px(getContext(), 2));
+            tickMarkMode = t.getInt(R.styleable.RangeSeekBar_rsb_tick_mark_mode, TRICK_MARK_MODE_NUMBER);
+            tickMarkGravity = t.getInt(R.styleable.RangeSeekBar_rsb_tick_mark_gravity, TICK_MARK_GRAVITY_CENTER);
+            tickMarkLayoutGravity = t.getInt(R.styleable.RangeSeekBar_rsb_tick_mark_layout_gravity, Gravity.TOP);
+            tickMarkTextArray = t.getTextArray(R.styleable.RangeSeekBar_rsb_tick_mark_text_array);
+            tickMarkTextMargin = (int) t.getDimension(R.styleable.RangeSeekBar_rsb_tick_mark_text_margin, Utils.dp2px(getContext(), 7));
+            tickMarkTextSize = (int) t.getDimension(R.styleable.RangeSeekBar_rsb_tick_mark_text_size, Utils.dp2px(getContext(), 12));
+            tickMarkTextColor = t.getColor(R.styleable.RangeSeekBar_rsb_tick_mark_text_color, progressDefaultColor);
+            tickMarkInRangeTextColor = t.getColor(R.styleable.RangeSeekBar_rsb_tick_mark_in_range_text_color, progressColor);
+            steps = t.getInt(R.styleable.RangeSeekBar_rsb_steps, 0);
+            stepsColor = t.getColor(R.styleable.RangeSeekBar_rsb_step_color, 0xFF9d9d9d);
+            stepsRadius = t.getDimension(R.styleable.RangeSeekBar_rsb_step_radius, 0);
+            stepsWidth = t.getDimension(R.styleable.RangeSeekBar_rsb_step_width, 0);
+            stepsHeight = t.getDimension(R.styleable.RangeSeekBar_rsb_step_height, 0);
+            stepsDrawableId = t.getResourceId(R.styleable.RangeSeekBar_rsb_step_drawable, 0);
+            stepsAutoBonding = t.getBoolean(R.styleable.RangeSeekBar_rsb_step_auto_bonding, true);
+            t.recycle();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+    }
+
+
+    /**
+     * measure progress bar position
+     */
+    protected void onMeasureProgress(int w, int h) {
+        int viewHeight = h - getPaddingBottom() - getPaddingTop();
+        if (h <= 0) return;
+
+        if (gravity == Gravity.TOP) {
+            //calculate the height of indicator and thumb exceeds the part of the progress
+            float maxIndicatorHeight = 0;
+            if (leftSB.getIndicatorShowMode() != INDICATOR_ALWAYS_HIDE
+                    || rightSB.getIndicatorShowMode() != INDICATOR_ALWAYS_HIDE) {
+                maxIndicatorHeight = Math.max(leftSB.getIndicatorRawHeight(), rightSB.getIndicatorRawHeight());
+            }
+            float thumbHeight = Math.max(leftSB.getThumbScaleHeight(), rightSB.getThumbScaleHeight());
+            thumbHeight -= progressHeight / 2f;
+
+            //default height is indicator + thumb exceeds the part of the progress bar
+            //if tickMark height is greater than (indicator + thumb exceeds the part of the progress)
+            progressTop = (int) (maxIndicatorHeight + (thumbHeight - progressHeight) / 2f);
+            if (tickMarkTextArray != null && tickMarkLayoutGravity == Gravity.TOP) {
+                progressTop = (int) Math.max(getTickMarkRawHeight(), maxIndicatorHeight + (thumbHeight - progressHeight) / 2f);
+            }
+            progressBottom = progressTop + progressHeight;
+        } else if (gravity == Gravity.BOTTOM) {
+            if (tickMarkTextArray != null && tickMarkLayoutGravity == Gravity.BOTTOM) {
+                progressBottom = viewHeight - getTickMarkRawHeight();
+            } else {
+                progressBottom = (int) (viewHeight - Math.max(leftSB.getThumbScaleHeight(), rightSB.getThumbScaleHeight()) / 2f
+                        + progressHeight / 2f);
+            }
+            progressTop = progressBottom - progressHeight;
+        } else {
+            progressTop = (viewHeight - progressHeight) / 2;
+            progressBottom = progressTop + progressHeight;
+        }
+
+        int maxThumbWidth = (int) Math.max(leftSB.getThumbScaleWidth(), rightSB.getThumbScaleWidth());
+        progressLeft = maxThumbWidth / 2 + getPaddingLeft();
+        progressRight = w - maxThumbWidth / 2 - getPaddingRight();
+        progressWidth = progressRight - progressLeft;
+        progressDefaultDstRect.set(getProgressLeft(), getProgressTop(), getProgressRight(), getProgressBottom());
+        progressPaddingRight = w - progressRight;
+        //default value
+        if (progressRadius <= 0) {
+            progressRadius = (int) ((getProgressBottom() - getProgressTop()) * 0.45f);
+        }
+        initProgressBitmap();
+    }
+
+    //Android 7.0以后,优化了View的绘制,onMeasure和onSizeChanged调用顺序有所变化
+    //Android7.0以下:onMeasure--->onSizeChanged--->onMeasure
+    //Android7.0以上:onMeasure--->onSizeChanged
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        /*
+         * onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值
+         * MeasureSpec.EXACTLY 是精确尺寸
+         * MeasureSpec.AT_MOST 是最大尺寸
+         * MeasureSpec.UNSPECIFIED 是未指定尺寸
+         */
+
+        if (heightMode == MeasureSpec.EXACTLY) {
+            heightSize = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
+        } else if (heightMode == MeasureSpec.AT_MOST && getParent() instanceof ViewGroup
+                && heightSize == ViewGroup.LayoutParams.MATCH_PARENT) {
+            heightSize = MeasureSpec.makeMeasureSpec(((ViewGroup) getParent()).getMeasuredHeight(), MeasureSpec.AT_MOST);
+        } else {
+            int heightNeeded;
+            if (gravity == Gravity.CENTER) {
+                if (tickMarkTextArray != null && tickMarkLayoutGravity == Gravity.BOTTOM) {
+                    heightNeeded = (int) (2 * (getRawHeight() - getTickMarkRawHeight()));
+                } else {
+                    heightNeeded = (int) (2 * (getRawHeight() - Math.max(leftSB.getThumbScaleHeight(), rightSB.getThumbScaleHeight()) / 2));
+                }
+            } else {
+                heightNeeded = (int) getRawHeight();
+            }
+            heightSize = MeasureSpec.makeMeasureSpec(heightNeeded, MeasureSpec.EXACTLY);
+        }
+        super.onMeasure(widthMeasureSpec, heightSize);
+    }
+
+    protected int getTickMarkRawHeight() {
+        if (tickMarkTextArray != null && tickMarkTextArray.length > 0) {
+            return tickMarkTextMargin + Utils.measureText(String.valueOf(tickMarkTextArray[0]), tickMarkTextSize).height() + 3;
+        }
+        return 0;
+    }
+
+    protected float getRawHeight() {
+        float rawHeight;
+        if (seekBarMode == SEEKBAR_MODE_SINGLE) {
+            rawHeight = leftSB.getRawHeight();
+            if (tickMarkLayoutGravity == Gravity.BOTTOM && tickMarkTextArray != null) {
+                float h = Math.max((leftSB.getThumbScaleHeight() - progressHeight) / 2, getTickMarkRawHeight());
+                rawHeight = rawHeight - leftSB.getThumbScaleHeight() / 2 + progressHeight / 2f + h;
+            }
+        } else {
+            rawHeight = Math.max(leftSB.getRawHeight(), rightSB.getRawHeight());
+            if (tickMarkLayoutGravity == Gravity.BOTTOM && tickMarkTextArray != null) {
+                float thumbHeight = Math.max(leftSB.getThumbScaleHeight(), rightSB.getThumbScaleHeight());
+                float h = Math.max((thumbHeight - progressHeight) / 2, getTickMarkRawHeight());
+                rawHeight = rawHeight - thumbHeight / 2 + progressHeight / 2f + h;
+            }
+        }
+        return rawHeight;
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        onMeasureProgress(w, h);
+        //set default value
+        setRange(minProgress, maxProgress, minInterval);
+        // initializes the positions of the two thumbs
+        int lineCenterY = (getProgressBottom() + getProgressTop()) / 2;
+        leftSB.onSizeChanged(getProgressLeft(), lineCenterY);
+        if (seekBarMode == SEEKBAR_MODE_RANGE) {
+            rightSB.onSizeChanged(getProgressLeft(), lineCenterY);
+        }
+    }
+
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        onDrawTickMark(canvas, paint);
+        onDrawProgressBar(canvas, paint);
+        onDrawSteps(canvas, paint);
+        onDrawSeekBar(canvas);
+    }
+
+    //绘制刻度,并且根据当前位置是否在刻度范围内设置不同的颜色显示
+    // Draw the scales, and according to the current position is set within
+    // the scale range of different color display
+    protected void onDrawTickMark(Canvas canvas, Paint paint) {
+        if (tickMarkTextArray != null) {
+            int trickPartWidth = progressWidth / (tickMarkTextArray.length - 1);
+            for (int i = 0; i < tickMarkTextArray.length; i++) {
+                final String text2Draw = tickMarkTextArray[i].toString();
+                if (TextUtils.isEmpty(text2Draw)) continue;
+                paint.getTextBounds(text2Draw, 0, text2Draw.length(), tickMarkTextRect);
+                paint.setColor(tickMarkTextColor);
+                //平分显示
+                float x;
+                if (tickMarkMode == TRICK_MARK_MODE_OTHER) {
+                    if (tickMarkGravity == TICK_MARK_GRAVITY_RIGHT) {
+                        x = getProgressLeft() + i * trickPartWidth - tickMarkTextRect.width();
+                    } else if (tickMarkGravity == TICK_MARK_GRAVITY_CENTER) {
+                        x = getProgressLeft() + i * trickPartWidth - tickMarkTextRect.width() / 2f;
+                    } else {
+                        x = getProgressLeft() + i * trickPartWidth;
+                    }
+                } else {
+                    float num = Utils.parseFloat(text2Draw);
+                    SeekBarState[] states = getRangeSeekBarState();
+                    if (Utils.compareFloat(num, states[0].value) != -1 && Utils.compareFloat(num, states[1].value) != 1 && (seekBarMode == SEEKBAR_MODE_RANGE)) {
+                        paint.setColor(tickMarkInRangeTextColor);
+                    }
+                    //按实际比例显示
+                    x = getProgressLeft() + progressWidth * (num - minProgress) / (maxProgress - minProgress)
+                            - tickMarkTextRect.width() / 2f;
+                }
+                float y;
+                if (tickMarkLayoutGravity == Gravity.TOP) {
+                    y = getProgressTop() - tickMarkTextMargin;
+                } else {
+                    y = getProgressBottom() + tickMarkTextMargin + tickMarkTextRect.height();
+                }
+                canvas.drawText(text2Draw, x, y, paint);
+            }
+        }
+    }
+
+    //绘制进度条
+    // draw the progress bar
+    protected void onDrawProgressBar(Canvas canvas, Paint paint) {
+
+        //draw default progress
+        if (Utils.verifyBitmap(progressDefaultBitmap)) {
+            canvas.drawBitmap(progressDefaultBitmap, null, progressDefaultDstRect, paint);
+        } else {
+            paint.setColor(progressDefaultColor);
+            canvas.drawRoundRect(progressDefaultDstRect, progressRadius, progressRadius, paint);
+        }
+
+        //draw progress
+        if (seekBarMode == SEEKBAR_MODE_RANGE) {
+            progressDstRect.top = getProgressTop();
+            progressDstRect.left = leftSB.left + leftSB.getThumbScaleWidth() / 2f + progressWidth * leftSB.currPercent;
+            progressDstRect.right = rightSB.left + rightSB.getThumbScaleWidth() / 2f + progressWidth * rightSB.currPercent;
+            progressDstRect.bottom = getProgressBottom();
+        } else {
+            progressDstRect.top = getProgressTop();
+            progressDstRect.left = leftSB.left + leftSB.getThumbScaleWidth() / 2f;
+            progressDstRect.right = leftSB.left + leftSB.getThumbScaleWidth() / 2f + progressWidth * leftSB.currPercent;
+            progressDstRect.bottom = getProgressBottom();
+        }
+
+        if (Utils.verifyBitmap(progressBitmap)) {
+            progressSrcRect.top = 0;
+            progressSrcRect.bottom = progressBitmap.getHeight();
+            int bitmapWidth = progressBitmap.getWidth();
+            if (seekBarMode == SEEKBAR_MODE_RANGE) {
+                progressSrcRect.left = (int) (bitmapWidth * leftSB.currPercent);
+                progressSrcRect.right = (int) (bitmapWidth * rightSB.currPercent);
+            } else {
+                progressSrcRect.left = 0;
+                progressSrcRect.right = (int) (bitmapWidth * leftSB.currPercent);
+            }
+            canvas.drawBitmap(progressBitmap, progressSrcRect, progressDstRect, null);
+        } else {
+            paint.setColor(progressColor);
+            canvas.drawRoundRect(progressDstRect, progressRadius, progressRadius, paint);
+        }
+
+    }
+
+    //draw steps
+    protected void onDrawSteps(Canvas canvas, Paint paint) {
+        if (!verifyStepsMode()) return;
+        int stepMarks = getProgressWidth() / (steps);
+        float extHeight = (stepsHeight - getProgressHeight()) / 2f;
+        for (int k = 0; k <= steps-1; k++) {
+            float x = getProgressLeft() + k * stepMarks - stepsWidth / 2f;
+            stepDivRect.set(x, getProgressTop() - extHeight, x + stepsWidth, getProgressBottom() + extHeight);
+            if (stepsBitmaps.isEmpty() || stepsBitmaps.size() <= k) {
+                paint.setColor(stepsColor);
+                canvas.drawRoundRect(stepDivRect, stepsRadius, stepsRadius, paint);
+            } else {
+                canvas.drawBitmap(stepsBitmaps.get(k), null, stepDivRect, paint);
+            }
+        }
+    }
+
+    //绘制SeekBar相关
+    protected void onDrawSeekBar(Canvas canvas) {
+        //draw left SeekBar
+        if (leftSB.getIndicatorShowMode() == INDICATOR_ALWAYS_SHOW) {
+            leftSB.setShowIndicatorEnable(true);
+        }
+        leftSB.draw(canvas);
+        //draw right SeekBar
+        if (seekBarMode == SEEKBAR_MODE_RANGE) {
+            if (rightSB.getIndicatorShowMode() == INDICATOR_ALWAYS_SHOW) {
+                rightSB.setShowIndicatorEnable(true);
+            }
+            rightSB.draw(canvas);
+        }
+    }
+
+    //初始化画笔
+    private void initPaint() {
+        paint.setStyle(Paint.Style.FILL);
+        paint.setColor(progressDefaultColor);
+        paint.setTextSize(tickMarkTextSize);
+        //抗锯齿
+        paint.setAntiAlias(true);
+    }
+
+
+    private void changeThumbActivateState(boolean hasActivate) {
+        if (hasActivate && currTouchSB != null) {
+            boolean state = currTouchSB == leftSB;
+            leftSB.setActivate(state);
+            if (seekBarMode == SEEKBAR_MODE_RANGE)
+                rightSB.setActivate(!state);
+        } else {
+            leftSB.setActivate(false);
+            if (seekBarMode == SEEKBAR_MODE_RANGE)
+                rightSB.setActivate(false);
+        }
+    }
+
+    protected float getEventX(MotionEvent event) {
+        return event.getX();
+    }
+
+    protected float getEventY(MotionEvent event) {
+        return event.getY();
+    }
+
+    /**
+     * scale the touch seekBar thumb
+     */
+    private void scaleCurrentSeekBarThumb() {
+        if (currTouchSB != null && currTouchSB.getThumbScaleRatio() > 1f && !isScaleThumb) {
+            isScaleThumb = true;
+            currTouchSB.scaleThumb();
+        }
+    }
+
+    /**
+     * reset the touch seekBar thumb
+     */
+    private void resetCurrentSeekBarThumb() {
+        if (currTouchSB != null && currTouchSB.getThumbScaleRatio() > 1f && isScaleThumb) {
+            isScaleThumb = false;
+            currTouchSB.resetThumb();
+        }
+    }
+
+    //calculate currTouchSB percent by MotionEvent
+    protected float calculateCurrentSeekBarPercent(float touchDownX) {
+        if (currTouchSB == null)return 0;
+        float percent = (touchDownX - getProgressLeft()) * 1f / (progressWidth);
+        if (touchDownX < getProgressLeft()) {
+            percent = 0;
+        } else if (touchDownX > getProgressRight()) {
+            percent = 1;
+        }
+        //RangeMode minimum interval
+        if (seekBarMode == SEEKBAR_MODE_RANGE) {
+            if (currTouchSB == leftSB) {
+                if (percent > rightSB.currPercent - reservePercent) {
+                    percent = rightSB.currPercent - reservePercent;
+                }
+            } else if (currTouchSB == rightSB) {
+                if (percent < leftSB.currPercent + reservePercent) {
+                    percent = leftSB.currPercent + reservePercent;
+                }
+            }
+        }
+        return percent;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (!isEnable) return true;
+
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                touchDownX = getEventX(event);
+                touchDownY = getEventY(event);
+                if (seekBarMode == SEEKBAR_MODE_RANGE) {
+                    if (rightSB.currPercent >= 1 && leftSB.collide(getEventX(event), getEventY(event))) {
+                        currTouchSB = leftSB;
+                        scaleCurrentSeekBarThumb();
+                    } else if (rightSB.collide(getEventX(event), getEventY(event))) {
+                        currTouchSB = rightSB;
+                        scaleCurrentSeekBarThumb();
+                    } else {
+                        float performClick = (touchDownX - getProgressLeft()) * 1f / (progressWidth);
+                        float distanceLeft = Math.abs(leftSB.currPercent - performClick);
+                        float distanceRight = Math.abs(rightSB.currPercent - performClick);
+                        if (distanceLeft < distanceRight) {
+                            currTouchSB = leftSB;
+                        } else {
+                            currTouchSB = rightSB;
+                        }
+                        performClick = calculateCurrentSeekBarPercent(touchDownX);
+                        currTouchSB.slide(performClick);
+                    }
+                } else {
+                    currTouchSB = leftSB;
+                    scaleCurrentSeekBarThumb();
+                }
+
+                //Intercept parent TouchEvent
+                if (getParent() != null) {
+                    getParent().requestDisallowInterceptTouchEvent(true);
+                }
+                if (callback != null) {
+                    callback.onStartTrackingTouch(this, currTouchSB == leftSB);
+                }
+                changeThumbActivateState(true);
+                return true;
+            case MotionEvent.ACTION_MOVE:
+                float x = getEventX(event);
+                if ((seekBarMode == SEEKBAR_MODE_RANGE) && leftSB.currPercent == rightSB.currPercent) {
+                    currTouchSB.materialRestore();
+                    if (callback != null) {
+                        callback.onStopTrackingTouch(this, currTouchSB == leftSB);
+                    }
+                    if (x - touchDownX > 0) {
+                        //method to move right
+                        if (currTouchSB != rightSB) {
+                            currTouchSB.setShowIndicatorEnable(false);
+                            resetCurrentSeekBarThumb();
+                            currTouchSB = rightSB;
+                        }
+                    } else {
+                        //method to move left
+                        if (currTouchSB != leftSB) {
+                            currTouchSB.setShowIndicatorEnable(false);
+                            resetCurrentSeekBarThumb();
+                            currTouchSB = leftSB;
+                        }
+                    }
+                    if (callback != null) {
+                        callback.onStartTrackingTouch(this, currTouchSB == leftSB);
+                    }
+                }
+                scaleCurrentSeekBarThumb();
+                currTouchSB.material = currTouchSB.material >= 1 ? 1 : currTouchSB.material + 0.1f;
+                touchDownX = x;
+                if (stepsAutoBonding) {
+                    float percent = calculateCurrentSeekBarPercent(getEventX(event));
+                    float stepPercent = 1.0f / steps;
+                    int stepSelected = new BigDecimal(percent / stepPercent).setScale(0, RoundingMode.HALF_UP).intValue();
+                    currTouchSB.slide(stepSelected * stepPercent);
+                }else {
+                    currTouchSB.slide(calculateCurrentSeekBarPercent(touchDownX));
+                }
+                currTouchSB.setShowIndicatorEnable(true);
+
+                if (callback != null) {
+                    SeekBarState[] states = getRangeSeekBarState();
+                    callback.onRangeChanged(this, states[0].value, states[1].value, true);
+                }
+                invalidate();
+                //Intercept parent TouchEvent
+                if (getParent() != null) {
+                    getParent().requestDisallowInterceptTouchEvent(true);
+                }
+                changeThumbActivateState(true);
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                if (seekBarMode == SEEKBAR_MODE_RANGE) {
+                    rightSB.setShowIndicatorEnable(false);
+                }
+                if (currTouchSB == leftSB) {
+                    resetCurrentSeekBarThumb();
+                } else if (currTouchSB == rightSB) {
+                    resetCurrentSeekBarThumb();
+                }
+                leftSB.setShowIndicatorEnable(false);
+                if (callback != null) {
+                    SeekBarState[] states = getRangeSeekBarState();
+                    callback.onRangeChanged(this, states[0].value, states[1].value, false);
+                }
+                //Intercept parent TouchEvent
+                if (getParent() != null) {
+                    getParent().requestDisallowInterceptTouchEvent(true);
+                }
+                changeThumbActivateState(false);
+                break;
+            case MotionEvent.ACTION_UP:
+                if (verifyStepsMode() && stepsAutoBonding) {
+                    float percent = calculateCurrentSeekBarPercent(getEventX(event));
+                    float stepPercent = 1.0f / steps;
+                    int stepSelected = new BigDecimal(percent / stepPercent).setScale(0, RoundingMode.HALF_UP).intValue();
+                    currTouchSB.slide(stepSelected * stepPercent);
+                }
+
+                if (seekBarMode == SEEKBAR_MODE_RANGE) {
+                    rightSB.setShowIndicatorEnable(false);
+                }
+                leftSB.setShowIndicatorEnable(false);
+                currTouchSB.materialRestore();
+                resetCurrentSeekBarThumb();
+                if (callback != null) {
+                    SeekBarState[] states = getRangeSeekBarState();
+                    callback.onRangeChanged(this, states[0].value, states[1].value, false);
+                }
+                //Intercept parent TouchEvent
+                if (getParent() != null) {
+                    getParent().requestDisallowInterceptTouchEvent(true);
+                }
+                if (callback != null) {
+                    callback.onStopTrackingTouch(this, currTouchSB == leftSB);
+                }
+                changeThumbActivateState(false);
+                break;
+        }
+        return super.onTouchEvent(event);
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState ss = new SavedState(superState);
+        ss.minValue = minProgress;
+        ss.maxValue = maxProgress;
+        ss.rangeInterval = minInterval;
+        SeekBarState[] results = getRangeSeekBarState();
+        ss.currSelectedMin = results[0].value;
+        ss.currSelectedMax = results[1].value;
+        return ss;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        try {
+            SavedState ss = (SavedState) state;
+            super.onRestoreInstanceState(ss.getSuperState());
+            float min = ss.minValue;
+            float max = ss.maxValue;
+            float rangeInterval = ss.rangeInterval;
+            setRange(min, max, rangeInterval);
+            float currSelectedMin = ss.currSelectedMin;
+            float currSelectedMax = ss.currSelectedMax;
+            setProgress(currSelectedMin, currSelectedMax);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+    }
+
+    //******************* Attributes getter and setter *******************//
+
+    public void setOnRangeChangedListener(OnRangeChangedListener listener) {
+        callback = listener;
+    }
+
+    public void setProgress(float value) {
+        setProgress(value, maxProgress);
+    }
+
+    public void setProgress(float leftValue, float rightValue) {
+        leftValue = Math.min(leftValue, rightValue);
+        rightValue = Math.max(leftValue, rightValue);
+        if (rightValue - leftValue < minInterval) {
+            leftValue = rightValue - minInterval;
+        }
+
+        if (leftValue < minProgress) {
+            leftValue = minProgress;
+            //throw new IllegalArgumentException("setProgress() min < (preset min - offsetValue) . #min:" + leftValue + " #preset min:" + rightValue);
+        }
+        if (rightValue > maxProgress) {
+            rightValue =maxProgress;
+            throw new IllegalArgumentException("setProgress() max > (preset max - offsetValue) . #max:" + rightValue + " #preset max:" + rightValue);
+        }
+
+        float range = maxProgress - minProgress;
+        leftSB.currPercent = Math.abs(leftValue - minProgress) / range;
+        if (seekBarMode == SEEKBAR_MODE_RANGE) {
+            rightSB.currPercent = Math.abs(rightValue - minProgress) / range;
+        }
+
+        if (callback != null) {
+            callback.onRangeChanged(this, leftValue, rightValue, false);
+        }
+        invalidate();
+    }
+
+
+    /**
+     * 设置范围
+     *
+     * @param min 最小值
+     * @param max 最大值
+     */
+    public void setRange(float min, float max) {
+        setRange(min, max, minInterval);
+    }
+
+    /**
+     * 设置范围
+     *
+     * @param min         最小值
+     * @param max         最大值
+     * @param minInterval 最小间隔
+     */
+    public void setRange(float min, float max, float minInterval) {
+        if (max <= min) {
+            throw new IllegalArgumentException("setRange() max must be greater than min ! #max:" + max + " #min:" + min);
+        }
+        if (minInterval < 0) {
+            throw new IllegalArgumentException("setRange() interval must be greater than zero ! #minInterval:" + minInterval);
+        }
+        if (minInterval >= max - min) {
+            throw new IllegalArgumentException("setRange() interval must be less than (max - min) ! #minInterval:" + minInterval + " #max - min:" + (max - min));
+        }
+
+        maxProgress = max;
+        minProgress = min;
+        this.minInterval = minInterval;
+        reservePercent = minInterval / (max - min);
+
+        //set default value
+        if (seekBarMode == SEEKBAR_MODE_RANGE) {
+            if (leftSB.currPercent + reservePercent <= 1 && leftSB.currPercent + reservePercent > rightSB.currPercent) {
+                rightSB.currPercent = leftSB.currPercent + reservePercent;
+            } else if (rightSB.currPercent - reservePercent >= 0 && rightSB.currPercent - reservePercent < leftSB.currPercent) {
+                leftSB.currPercent = rightSB.currPercent - reservePercent;
+            }
+        }
+        invalidate();
+    }
+
+    /**
+     * @return the two seekBar state , see {@link com.jaygoo.widget.SeekBarState}
+     */
+    public SeekBarState[] getRangeSeekBarState() {
+        SeekBarState leftSeekBarState = new SeekBarState();
+        leftSeekBarState.value = leftSB.getProgress();
+
+        leftSeekBarState.indicatorText = String.valueOf(leftSeekBarState.value);
+        if (Utils.compareFloat(leftSeekBarState.value, minProgress) == 0) {
+            leftSeekBarState.isMin = true;
+        } else if (Utils.compareFloat(leftSeekBarState.value, maxProgress) == 0) {
+            leftSeekBarState.isMax = true;
+        }
+
+        SeekBarState rightSeekBarState = new SeekBarState();
+        if (seekBarMode == SEEKBAR_MODE_RANGE) {
+            rightSeekBarState.value = rightSB.getProgress();
+            rightSeekBarState.indicatorText = String.valueOf(rightSeekBarState.value);
+            if (Utils.compareFloat(rightSB.currPercent, minProgress) == 0) {
+                rightSeekBarState.isMin = true;
+            } else if (Utils.compareFloat(rightSB.currPercent, maxProgress) == 0) {
+                rightSeekBarState.isMax = true;
+            }
+        }
+
+        return new SeekBarState[]{leftSeekBarState, rightSeekBarState};
+    }
+
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        super.setEnabled(enabled);
+        this.isEnable = enabled;
+    }
+
+    public void setIndicatorText(String progress) {
+        leftSB.setIndicatorText(progress);
+        if (seekBarMode == SEEKBAR_MODE_RANGE) {
+            rightSB.setIndicatorText(progress);
+        }
+    }
+
+    /**
+     * format number indicator text
+     *
+     * @param formatPattern format rules
+     */
+    public void setIndicatorTextDecimalFormat(String formatPattern) {
+        leftSB.setIndicatorTextDecimalFormat(formatPattern);
+        if (seekBarMode == SEEKBAR_MODE_RANGE) {
+            rightSB.setIndicatorTextDecimalFormat(formatPattern);
+        }
+    }
+
+    /**
+     * format string indicator text
+     *
+     * @param formatPattern format rules
+     */
+    public void setIndicatorTextStringFormat(String formatPattern) {
+        leftSB.setIndicatorTextStringFormat(formatPattern);
+        if (seekBarMode == SEEKBAR_MODE_RANGE) {
+            rightSB.setIndicatorTextStringFormat(formatPattern);
+        }
+    }
+
+    /**
+     * if is single mode, please use it to get the SeekBar
+     *
+     * @return left seek bar
+     */
+    public SeekBar getLeftSeekBar() {
+        return leftSB;
+    }
+
+    public SeekBar getRightSeekBar() {
+        return rightSB;
+    }
+
+
+    public int getProgressTop() {
+        return progressTop;
+    }
+
+    public int getProgressBottom() {
+        return progressBottom;
+    }
+
+    public int getProgressLeft() {
+        return progressLeft;
+    }
+
+    public int getProgressRight() {
+        return progressRight;
+    }
+
+    public int getProgressPaddingRight() {
+        return progressPaddingRight;
+    }
+
+    public int getProgressHeight() {
+        return progressHeight;
+    }
+
+    public void setProgressHeight(int progressHeight) {
+        this.progressHeight = progressHeight;
+    }
+
+    public float getMinProgress() {
+        return minProgress;
+    }
+
+    public float getMaxProgress() {
+        return maxProgress;
+    }
+
+    public void setProgressColor(@ColorInt int progressDefaultColor, @ColorInt int progressColor) {
+        this.progressDefaultColor = progressDefaultColor;
+        this.progressColor = progressColor;
+    }
+
+    public int getTickMarkTextColor() {
+        return tickMarkTextColor;
+    }
+
+    public void setTickMarkTextColor(@ColorInt int tickMarkTextColor) {
+        this.tickMarkTextColor = tickMarkTextColor;
+    }
+
+    public int getTickMarkInRangeTextColor() {
+        return tickMarkInRangeTextColor;
+    }
+
+    public void setTickMarkInRangeTextColor(@ColorInt int tickMarkInRangeTextColor) {
+        this.tickMarkInRangeTextColor = tickMarkInRangeTextColor;
+    }
+
+    public int getSeekBarMode() {
+        return seekBarMode;
+    }
+
+    /**
+     * {@link #SEEKBAR_MODE_SINGLE} is single SeekBar
+     * {@link #SEEKBAR_MODE_RANGE} is range SeekBar
+     * @param seekBarMode
+     */
+    public void setSeekBarMode(@SeekBarModeDef int seekBarMode) {
+        this.seekBarMode = seekBarMode;
+        rightSB.setVisible(seekBarMode != SEEKBAR_MODE_SINGLE);
+    }
+
+    public int getTickMarkMode() {
+        return tickMarkMode;
+    }
+
+    /**
+     * {@link #TICK_MARK_GRAVITY_LEFT} is number tick mark, it will locate the position according to the value.
+     * {@link #TICK_MARK_GRAVITY_RIGHT} is text tick mark, it will be equally positioned.
+     * @param tickMarkMode
+     */
+    public void setTickMarkMode(@TickMarkModeDef int tickMarkMode) {
+        this.tickMarkMode = tickMarkMode;
+    }
+
+    public int getTickMarkTextMargin() {
+        return tickMarkTextMargin;
+    }
+
+    public void setTickMarkTextMargin(int tickMarkTextMargin) {
+        this.tickMarkTextMargin = tickMarkTextMargin;
+    }
+
+    public int getTickMarkTextSize() {
+        return tickMarkTextSize;
+    }
+
+    public void setTickMarkTextSize(int tickMarkTextSize) {
+        this.tickMarkTextSize = tickMarkTextSize;
+    }
+
+    public int getTickMarkGravity() {
+        return tickMarkGravity;
+    }
+
+    /**
+     * the tick mark text gravity
+     * {@link #TICK_MARK_GRAVITY_LEFT}
+     * {@link #TICK_MARK_GRAVITY_RIGHT}
+     * {@link #TICK_MARK_GRAVITY_CENTER}
+     * @param tickMarkGravity
+     */
+    public void setTickMarkGravity(@TickMarkGravityDef int tickMarkGravity) {
+        this.tickMarkGravity = tickMarkGravity;
+    }
+
+    public CharSequence[] getTickMarkTextArray() {
+        return tickMarkTextArray;
+    }
+
+    public void setTickMarkTextArray(CharSequence[] tickMarkTextArray) {
+        this.tickMarkTextArray = tickMarkTextArray;
+    }
+
+    public float getMinInterval() {
+        return minInterval;
+    }
+
+    public float getProgressRadius() {
+        return progressRadius;
+    }
+
+    public void setProgressRadius(float progressRadius) {
+        this.progressRadius = progressRadius;
+    }
+
+    public int getProgressColor() {
+        return progressColor;
+    }
+
+    public void setProgressColor(@ColorInt int progressColor) {
+        this.progressColor = progressColor;
+    }
+
+    public int getProgressDefaultColor() {
+        return progressDefaultColor;
+    }
+
+    public void setProgressDefaultColor(@ColorInt int progressDefaultColor) {
+        this.progressDefaultColor = progressDefaultColor;
+    }
+
+    public int getProgressDrawableId() {
+        return progressDrawableId;
+    }
+
+    public void setProgressDrawableId(@DrawableRes int progressDrawableId) {
+        this.progressDrawableId = progressDrawableId;
+        progressBitmap = null;
+        initProgressBitmap();
+    }
+
+    public int getProgressDefaultDrawableId() {
+        return progressDefaultDrawableId;
+    }
+
+    public void setProgressDefaultDrawableId(@DrawableRes int progressDefaultDrawableId) {
+        this.progressDefaultDrawableId = progressDefaultDrawableId;
+        progressDefaultBitmap = null;
+        initProgressBitmap();
+    }
+
+    public int getProgressWidth() {
+        return progressWidth;
+    }
+
+    public void setProgressWidth(int progressWidth) {
+        this.progressWidth = progressWidth;
+    }
+
+
+    public void setTypeface(Typeface typeFace) {
+        paint.setTypeface(typeFace);
+    }
+
+    public boolean isEnableThumbOverlap() {
+        return enableThumbOverlap;
+    }
+
+    public void setEnableThumbOverlap(boolean enableThumbOverlap) {
+        this.enableThumbOverlap = enableThumbOverlap;
+    }
+
+    public void setSteps(int steps) {
+        this.steps = steps;
+    }
+
+    public int getSteps() {
+        return steps;
+    }
+
+    public int getStepsColor() {
+        return stepsColor;
+    }
+
+    public void setStepsColor(@ColorInt int stepsColor) {
+        this.stepsColor = stepsColor;
+    }
+
+    public float getStepsWidth() {
+        return stepsWidth;
+    }
+
+    public void setStepsWidth(float stepsWidth) {
+        this.stepsWidth = stepsWidth;
+    }
+
+    public float getStepsHeight() {
+        return stepsHeight;
+    }
+
+    public void setStepsHeight(float stepsHeight) {
+        this.stepsHeight = stepsHeight;
+    }
+
+    public float getStepsRadius() {
+        return stepsRadius;
+    }
+
+    public void setStepsRadius(float stepsRadius) {
+        this.stepsRadius = stepsRadius;
+    }
+
+    public void setProgressTop(int progressTop) {
+        this.progressTop = progressTop;
+    }
+
+    public void setProgressBottom(int progressBottom) {
+        this.progressBottom = progressBottom;
+    }
+
+    public void setProgressLeft(int progressLeft) {
+        this.progressLeft = progressLeft;
+    }
+
+    public void setProgressRight(int progressRight) {
+        this.progressRight = progressRight;
+    }
+
+    public int getTickMarkLayoutGravity() {
+        return tickMarkLayoutGravity;
+    }
+
+    /**
+     * the tick mark layout gravity
+     * Gravity.TOP and Gravity.BOTTOM
+     * @param tickMarkLayoutGravity
+     */
+    public void setTickMarkLayoutGravity(@TickMarkLayoutGravityDef int tickMarkLayoutGravity) {
+        this.tickMarkLayoutGravity = tickMarkLayoutGravity;
+    }
+
+    public int getGravity() {
+        return gravity;
+    }
+
+    /**
+     * the RangeSeekBar gravity
+     * Gravity.TOP and Gravity.BOTTOM
+     * @param gravity
+     */
+    public void setGravity(@GravityDef int gravity) {
+        this.gravity = gravity;
+    }
+
+    public boolean isStepsAutoBonding() {
+        return stepsAutoBonding;
+    }
+
+    public void setStepsAutoBonding(boolean stepsAutoBonding) {
+        this.stepsAutoBonding = stepsAutoBonding;
+    }
+
+    public int getStepsDrawableId() {
+        return stepsDrawableId;
+    }
+
+    public void setStepsDrawableId(@DrawableRes int stepsDrawableId) {
+        this.stepsBitmaps.clear();
+        this.stepsDrawableId = stepsDrawableId;
+        initStepsBitmap();
+    }
+
+    public List<Bitmap> getStepsBitmaps() {
+        return stepsBitmaps;
+    }
+
+    public void setStepsBitmaps(List<Bitmap> stepsBitmaps) {
+        if (stepsBitmaps == null || stepsBitmaps.isEmpty() || stepsBitmaps.size() <= steps) {
+            throw new IllegalArgumentException("stepsBitmaps must > steps !");
+        }
+        this.stepsBitmaps.clear();
+        this.stepsBitmaps.addAll(stepsBitmaps);
+    }
+
+    public void setStepsDrawable(List<Integer> stepsDrawableIds) {
+        if (stepsDrawableIds == null || stepsDrawableIds.isEmpty() || stepsDrawableIds.size() <= steps) {
+            throw new IllegalArgumentException("stepsDrawableIds must > steps !");
+        }
+        if (!verifyStepsMode()) {
+            throw new IllegalArgumentException("stepsWidth must > 0, stepsHeight must > 0,steps must > 0 First!!");
+        }
+        List<Bitmap> stepsBitmaps = new ArrayList<>();
+        for (int i = 0; i < stepsDrawableIds.size(); i++) {
+            stepsBitmaps.add(Utils.drawableToBitmap(getContext(), (int) stepsWidth, (int) stepsHeight, stepsDrawableIds.get(i)));
+        }
+        setStepsBitmaps(stepsBitmaps);
+    }
+}

+ 57 - 0
RangeSeekBar/src/main/java/com/jaygoo/widget/SavedState.java

@@ -0,0 +1,57 @@
+package com.jaygoo.widget;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.view.View;
+
+/**
+ * ================================================
+ * 作    者:JayGoo
+ * 版    本:
+ * 创建日期:2018/5/8
+ * 描    述:
+ * ================================================
+ */
+public class SavedState extends View.BaseSavedState {
+    public float minValue;
+    public float maxValue;
+    public float rangeInterval;
+    public int tickNumber;
+    public float currSelectedMin;
+    public float currSelectedMax;
+
+    public SavedState(Parcelable superState) {
+        super(superState);
+    }
+
+    private SavedState(Parcel in) {
+        super(in);
+        minValue = in.readFloat();
+        maxValue = in.readFloat();
+        rangeInterval = in.readFloat();
+        tickNumber = in.readInt();
+        currSelectedMin = in.readFloat();
+        currSelectedMax = in.readFloat();
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        super.writeToParcel(out, flags);
+        out.writeFloat(minValue);
+        out.writeFloat(maxValue);
+        out.writeFloat(rangeInterval);
+        out.writeInt(tickNumber);
+        out.writeFloat(currSelectedMin);
+        out.writeFloat(currSelectedMax);
+    }
+
+    public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
+        public SavedState createFromParcel(Parcel in) {
+            return new SavedState(in);
+        }
+
+        public SavedState[] newArray(int size) {
+            return new SavedState[size];
+        }
+    };
+}

+ 691 - 0
RangeSeekBar/src/main/java/com/jaygoo/widget/SeekBar.java

@@ -0,0 +1,691 @@
+package com.jaygoo.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.os.Build;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.IntDef;
+import androidx.core.content.ContextCompat;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+
+import java.text.DecimalFormat;
+
+
+/**
+ * ================================================
+ * 作    者:JayGoo
+ * 版    本:
+ * 创建日期:2018/5/8
+ * 描    述:
+ * ================================================
+ */
+
+public class SeekBar {
+    //the indicator show mode
+    public static final int INDICATOR_SHOW_WHEN_TOUCH = 0;
+    public static final int INDICATOR_ALWAYS_HIDE = 1;
+    public static final int INDICATOR_ALWAYS_SHOW_AFTER_TOUCH = 2;
+    public static final int INDICATOR_ALWAYS_SHOW = 3;
+
+    @IntDef({INDICATOR_SHOW_WHEN_TOUCH, INDICATOR_ALWAYS_HIDE, INDICATOR_ALWAYS_SHOW_AFTER_TOUCH, INDICATOR_ALWAYS_SHOW})
+    public @interface IndicatorModeDef {
+    }
+
+    public static final int WRAP_CONTENT = -1;
+    public static final int MATCH_PARENT = -2;
+
+    private int indicatorShowMode;
+
+    //进度提示背景的高度,宽度如果是0的话会自适应调整
+    //Progress prompted the background height, width,
+    private int indicatorHeight;
+    private int indicatorWidth;
+    //进度提示背景与按钮之间的距离
+    //The progress indicates the distance between the background and the button
+    private int indicatorMargin;
+    private int indicatorDrawableId;
+    private int indicatorArrowSize;
+    private int indicatorTextSize;
+    private int indicatorTextColor;
+    private float indicatorRadius;
+    private int indicatorBackgroundColor;
+    private int indicatorPaddingLeft, indicatorPaddingRight, indicatorPaddingTop, indicatorPaddingBottom;
+    private int thumbDrawableId;
+    private int thumbInactivatedDrawableId;
+    private int thumbWidth;
+    private int thumbHeight;
+
+    //when you touch or move, the thumb will scale, default not scale
+    float thumbScaleRatio;
+
+    //****************** the above is attr value  ******************//
+
+    int left, right, top, bottom;
+    float currPercent;
+    float material = 0;
+    private boolean isShowIndicator;
+    boolean isLeft;
+    Bitmap thumbBitmap;
+    Bitmap thumbInactivatedBitmap;
+    Bitmap indicatorBitmap;
+    ValueAnimator anim;
+    String userText2Draw;
+    boolean isActivate = false;
+    boolean isVisible = true;
+    RangeSeekBar rangeSeekBar;
+    String indicatorTextStringFormat;
+    Path indicatorArrowPath = new Path();
+    Rect indicatorTextRect = new Rect();
+    Rect indicatorRect = new Rect();
+    Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    DecimalFormat indicatorTextDecimalFormat;
+    int scaleThumbWidth;
+    int scaleThumbHeight;
+
+    public SeekBar(RangeSeekBar rangeSeekBar, AttributeSet attrs, boolean isLeft) {
+        this.rangeSeekBar = rangeSeekBar;
+        this.isLeft = isLeft;
+        initAttrs(attrs);
+        initBitmap();
+        initVariables();
+    }
+
+    private void initAttrs(AttributeSet attrs) {
+        TypedArray t = getContext().obtainStyledAttributes(attrs, R.styleable.RangeSeekBar);
+        if (t == null) return;
+        indicatorMargin = (int) t.getDimension(R.styleable.RangeSeekBar_rsb_indicator_margin, 0);
+        indicatorDrawableId = t.getResourceId(R.styleable.RangeSeekBar_rsb_indicator_drawable, 0);
+        indicatorShowMode = t.getInt(R.styleable.RangeSeekBar_rsb_indicator_show_mode, INDICATOR_ALWAYS_HIDE);
+        indicatorHeight = t.getLayoutDimension(R.styleable.RangeSeekBar_rsb_indicator_height, WRAP_CONTENT);
+        indicatorWidth = t.getLayoutDimension(R.styleable.RangeSeekBar_rsb_indicator_width, WRAP_CONTENT);
+        indicatorTextSize = (int) t.getDimension(R.styleable.RangeSeekBar_rsb_indicator_text_size, Utils.dp2px(getContext(), 14));
+        indicatorTextColor = t.getColor(R.styleable.RangeSeekBar_rsb_indicator_text_color, Color.WHITE);
+        indicatorBackgroundColor = t.getColor(R.styleable.RangeSeekBar_rsb_indicator_background_color, ContextCompat.getColor(getContext(), R.color.colorAccent));
+        indicatorPaddingLeft = (int) t.getDimension(R.styleable.RangeSeekBar_rsb_indicator_padding_left, 0);
+        indicatorPaddingRight = (int) t.getDimension(R.styleable.RangeSeekBar_rsb_indicator_padding_right, 0);
+        indicatorPaddingTop = (int) t.getDimension(R.styleable.RangeSeekBar_rsb_indicator_padding_top, 0);
+        indicatorPaddingBottom = (int) t.getDimension(R.styleable.RangeSeekBar_rsb_indicator_padding_bottom, 0);
+        indicatorArrowSize = (int) t.getDimension(R.styleable.RangeSeekBar_rsb_indicator_arrow_size, 0);
+        thumbDrawableId = t.getResourceId(R.styleable.RangeSeekBar_rsb_thumb_drawable, R.drawable.rsb_default_thumb);
+        thumbInactivatedDrawableId = t.getResourceId(R.styleable.RangeSeekBar_rsb_thumb_inactivated_drawable, 0);
+        thumbWidth = (int) t.getDimension(R.styleable.RangeSeekBar_rsb_thumb_width, Utils.dp2px(getContext(), 26));
+        thumbHeight = (int) t.getDimension(R.styleable.RangeSeekBar_rsb_thumb_height, Utils.dp2px(getContext(), 26));
+        thumbScaleRatio = t.getFloat(R.styleable.RangeSeekBar_rsb_thumb_scale_ratio, 1f);
+        indicatorRadius = t.getDimension(R.styleable.RangeSeekBar_rsb_indicator_radius, 0f);
+        t.recycle();
+    }
+
+    protected void initVariables() {
+        scaleThumbWidth = thumbWidth;
+        scaleThumbHeight = thumbHeight;
+        if (indicatorHeight == WRAP_CONTENT) {
+            indicatorHeight = Utils.measureText("8", indicatorTextSize).height() + indicatorPaddingTop + indicatorPaddingBottom;
+        }
+        if (indicatorArrowSize <= 0) {
+            indicatorArrowSize = (int) (thumbWidth / 4);
+        }
+    }
+
+    public Context getContext() {
+        return rangeSeekBar.getContext();
+    }
+
+    public Resources getResources() {
+        if (getContext() != null) return getContext().getResources();
+        return null;
+    }
+
+    /**
+     * 初始化进度提示的背景
+     */
+    private void initBitmap() {
+        setIndicatorDrawableId(indicatorDrawableId);
+        setThumbDrawableId(thumbDrawableId, thumbWidth, thumbHeight);
+        setThumbInactivatedDrawableId(thumbInactivatedDrawableId, thumbWidth, thumbHeight);
+    }
+
+    /**
+     * 计算每个按钮的位置和尺寸
+     * Calculates the position and size of each button
+     *
+     * @param x position x
+     * @param y position y
+     */
+    protected void onSizeChanged(int x, int y) {
+        initVariables();
+        initBitmap();
+        left = (int) (x - getThumbScaleWidth() / 2);
+        right = (int) (x + getThumbScaleWidth() / 2);
+        top = y - getThumbHeight() / 2;
+        bottom = y + getThumbHeight() / 2;
+    }
+
+
+    public void scaleThumb() {
+        scaleThumbWidth = (int) getThumbScaleWidth();
+        scaleThumbHeight = (int) getThumbScaleHeight();
+        int y = rangeSeekBar.getProgressBottom();
+        top = y - scaleThumbHeight / 2;
+        bottom = y + scaleThumbHeight / 2;
+        setThumbDrawableId(thumbDrawableId, scaleThumbWidth, scaleThumbHeight);
+    }
+
+    public void resetThumb() {
+        scaleThumbWidth = getThumbWidth();
+        scaleThumbHeight = getThumbHeight();
+        int y = rangeSeekBar.getProgressBottom();
+        top = y - scaleThumbHeight / 2;
+        bottom = y + scaleThumbHeight / 2;
+        setThumbDrawableId(thumbDrawableId, scaleThumbWidth, scaleThumbHeight);
+    }
+
+    public float getRawHeight() {
+        return getIndicatorHeight() + getIndicatorArrowSize() + getIndicatorMargin() + getThumbScaleHeight();
+    }
+
+    /**
+     * 绘制按钮和提示背景和文字
+     * Draw buttons and tips for background and text
+     *
+     * @param canvas Canvas
+     */
+    protected void draw(Canvas canvas) {
+        if (!isVisible) {
+            return;
+        }
+        int offset = (int) (rangeSeekBar.getProgressWidth() * currPercent);
+        canvas.save();
+        canvas.translate(offset, 0);
+        // translate canvas, then don't care left
+        canvas.translate(left, 0);
+        if (isShowIndicator) {
+            onDrawIndicator(canvas, paint, formatCurrentIndicatorText(userText2Draw));
+        }
+        onDrawThumb(canvas);
+        canvas.restore();
+    }
+
+
+    /**
+     * 绘制按钮
+     * 如果没有图片资源,则绘制默认按钮
+     * <p>
+     * draw the thumb button
+     * If there is no image resource, draw the default button
+     *
+     * @param canvas canvas
+     */
+    protected void onDrawThumb(Canvas canvas) {
+        if (thumbInactivatedBitmap != null && !isActivate) {
+            canvas.drawBitmap(thumbInactivatedBitmap, 0, rangeSeekBar.getProgressTop() + (rangeSeekBar.getProgressHeight() - scaleThumbHeight) / 2f, null);
+        } else if (thumbBitmap != null) {
+            canvas.drawBitmap(thumbBitmap, 0, rangeSeekBar.getProgressTop() + (rangeSeekBar.getProgressHeight() - scaleThumbHeight) / 2f, null);
+        }
+    }
+
+    /**
+     * 格式化提示文字
+     * format the indicator text
+     *
+     * @param text2Draw
+     * @return
+     */
+    protected String formatCurrentIndicatorText(String text2Draw) {
+        SeekBarState[] states = rangeSeekBar.getRangeSeekBarState();
+        if (TextUtils.isEmpty(text2Draw)) {
+            if (isLeft) {
+                if (indicatorTextDecimalFormat != null) {
+                    text2Draw = indicatorTextDecimalFormat.format(states[0].value);
+                } else {
+                    text2Draw = states[0].indicatorText;
+                }
+            } else {
+                if (indicatorTextDecimalFormat != null) {
+                    text2Draw = indicatorTextDecimalFormat.format(states[1].value);
+                } else {
+                    text2Draw = states[1].indicatorText;
+                }
+            }
+        }
+        if (indicatorTextStringFormat != null) {
+            text2Draw = String.format(indicatorTextStringFormat, text2Draw);
+        }
+        return text2Draw;
+    }
+
+    /**
+     * This method will draw the indicator background dynamically according to the text.
+     * you can use to set padding
+     *
+     * @param canvas    Canvas
+     * @param text2Draw Indicator text
+     */
+    protected void onDrawIndicator(Canvas canvas, Paint paint, String text2Draw) {
+        if (text2Draw == null) return;
+        paint.setTextSize(indicatorTextSize);
+        paint.setStyle(Paint.Style.FILL);
+        paint.setColor(indicatorBackgroundColor);
+        paint.getTextBounds(text2Draw, 0, text2Draw.length(), indicatorTextRect);
+        int realIndicatorWidth = indicatorTextRect.width() + indicatorPaddingLeft + indicatorPaddingRight;
+        if (indicatorWidth > realIndicatorWidth) {
+            realIndicatorWidth = indicatorWidth;
+        }
+
+        int realIndicatorHeight = indicatorTextRect.height() + indicatorPaddingTop + indicatorPaddingBottom;
+        if (indicatorHeight > realIndicatorHeight) {
+            realIndicatorHeight = indicatorHeight;
+        }
+
+        indicatorRect.left = (int) (scaleThumbWidth / 2f - realIndicatorWidth / 2f);
+        indicatorRect.top = bottom - realIndicatorHeight - scaleThumbHeight - indicatorMargin;
+        indicatorRect.right = indicatorRect.left + realIndicatorWidth;
+        indicatorRect.bottom = indicatorRect.top + realIndicatorHeight;
+        //draw default indicator arrow
+        if (indicatorBitmap == null) {
+            //arrow three point
+            //  b   c
+            //    a
+            int ax = scaleThumbWidth / 2;
+            int ay = indicatorRect.bottom;
+            int bx = ax - indicatorArrowSize;
+            int by = ay - indicatorArrowSize;
+            int cx = ax + indicatorArrowSize;
+            indicatorArrowPath.reset();
+            indicatorArrowPath.moveTo(ax, ay);
+            indicatorArrowPath.lineTo(bx, by);
+            indicatorArrowPath.lineTo(cx, by);
+            indicatorArrowPath.close();
+            canvas.drawPath(indicatorArrowPath, paint);
+            indicatorRect.bottom -= indicatorArrowSize;
+            indicatorRect.top -= indicatorArrowSize;
+        }
+
+        //indicator background edge processing
+        int defaultPaddingOffset = Utils.dp2px(getContext(), 1);
+        int leftOffset = indicatorRect.width() / 2 - (int) (rangeSeekBar.getProgressWidth() * currPercent) - rangeSeekBar.getProgressLeft() + defaultPaddingOffset;
+        int rightOffset = indicatorRect.width() / 2 - (int) (rangeSeekBar.getProgressWidth() * (1 - currPercent)) - rangeSeekBar.getProgressPaddingRight() + defaultPaddingOffset;
+
+        if (leftOffset > 0) {
+            indicatorRect.left += leftOffset;
+            indicatorRect.right += leftOffset;
+        } else if (rightOffset > 0) {
+            indicatorRect.left -= rightOffset;
+            indicatorRect.right -= rightOffset;
+        }
+
+        //draw indicator background
+        if (indicatorBitmap != null) {
+            Utils.drawBitmap(canvas, paint, indicatorBitmap, indicatorRect);
+        } else if (indicatorRadius > 0f) {
+            canvas.drawRoundRect(new RectF(indicatorRect), indicatorRadius, indicatorRadius, paint);
+        } else {
+            canvas.drawRect(indicatorRect, paint);
+        }
+
+        //draw indicator content text
+        int tx, ty;
+        if (indicatorPaddingLeft > 0) {
+            tx = indicatorRect.left + indicatorPaddingLeft;
+        } else if (indicatorPaddingRight > 0) {
+            tx = indicatorRect.right - indicatorPaddingRight - indicatorTextRect.width();
+        } else {
+            tx = indicatorRect.left + (realIndicatorWidth - indicatorTextRect.width()) / 2;
+        }
+
+        if (indicatorPaddingTop > 0) {
+            ty = indicatorRect.top + indicatorTextRect.height() + indicatorPaddingTop;
+        } else if (indicatorPaddingBottom > 0) {
+            ty = indicatorRect.bottom - indicatorTextRect.height() - indicatorPaddingBottom;
+        } else {
+            ty = indicatorRect.bottom - (realIndicatorHeight - indicatorTextRect.height()) / 2 + 1;
+        }
+
+        //draw indicator text
+        paint.setColor(indicatorTextColor);
+        canvas.drawText(text2Draw, tx, ty, paint);
+    }
+
+    /**
+     * 拖动检测
+     *
+     * @return is collide
+     */
+    protected boolean collide(float x, float y) {
+        int offset = (int) (rangeSeekBar.getProgressWidth() * currPercent);
+        return x > left + offset && x < right + offset && y > top && y < bottom;
+    }
+
+    protected void slide(float percent) {
+        if (percent < 0) percent = 0;
+        else if (percent > 1) percent = 1;
+        currPercent = percent;
+    }
+
+    protected void setShowIndicatorEnable(boolean isEnable) {
+        switch (indicatorShowMode) {
+            case INDICATOR_SHOW_WHEN_TOUCH:
+                isShowIndicator = isEnable;
+                break;
+            case INDICATOR_ALWAYS_SHOW:
+            case INDICATOR_ALWAYS_SHOW_AFTER_TOUCH:
+                isShowIndicator = true;
+                break;
+            case INDICATOR_ALWAYS_HIDE:
+                isShowIndicator = false;
+                break;
+        }
+    }
+
+    public void materialRestore() {
+        if (anim != null) anim.cancel();
+        anim = ValueAnimator.ofFloat(material, 0);
+        anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                material = (float) animation.getAnimatedValue();
+                if (rangeSeekBar != null) rangeSeekBar.invalidate();
+            }
+        });
+        anim.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                material = 0;
+                if (rangeSeekBar != null) rangeSeekBar.invalidate();
+            }
+        });
+        anim.start();
+    }
+
+    public void setIndicatorText(String text) {
+        userText2Draw = text;
+    }
+
+    public void setIndicatorTextDecimalFormat(String formatPattern) {
+        indicatorTextDecimalFormat = new DecimalFormat(formatPattern);
+    }
+
+    public DecimalFormat getIndicatorTextDecimalFormat() {
+        return indicatorTextDecimalFormat;
+    }
+
+    public void setIndicatorTextStringFormat(String formatPattern) {
+        indicatorTextStringFormat = formatPattern;
+    }
+
+    public int getIndicatorDrawableId() {
+        return indicatorDrawableId;
+    }
+
+    public void setIndicatorDrawableId(@DrawableRes int indicatorDrawableId) {
+        if (indicatorDrawableId != 0) {
+            this.indicatorDrawableId = indicatorDrawableId;
+            indicatorBitmap = BitmapFactory.decodeResource(getResources(), indicatorDrawableId);
+        }
+    }
+
+
+    public int getIndicatorArrowSize() {
+        return indicatorArrowSize;
+    }
+
+    public void setIndicatorArrowSize(int indicatorArrowSize) {
+        this.indicatorArrowSize = indicatorArrowSize;
+    }
+
+    public int getIndicatorPaddingLeft() {
+        return indicatorPaddingLeft;
+    }
+
+    public void setIndicatorPaddingLeft(int indicatorPaddingLeft) {
+        this.indicatorPaddingLeft = indicatorPaddingLeft;
+    }
+
+    public int getIndicatorPaddingRight() {
+        return indicatorPaddingRight;
+    }
+
+    public void setIndicatorPaddingRight(int indicatorPaddingRight) {
+        this.indicatorPaddingRight = indicatorPaddingRight;
+    }
+
+    public int getIndicatorPaddingTop() {
+        return indicatorPaddingTop;
+    }
+
+    public void setIndicatorPaddingTop(int indicatorPaddingTop) {
+        this.indicatorPaddingTop = indicatorPaddingTop;
+    }
+
+    public int getIndicatorPaddingBottom() {
+        return indicatorPaddingBottom;
+    }
+
+    public void setIndicatorPaddingBottom(int indicatorPaddingBottom) {
+        this.indicatorPaddingBottom = indicatorPaddingBottom;
+    }
+
+    public int getIndicatorMargin() {
+        return indicatorMargin;
+    }
+
+    public void setIndicatorMargin(int indicatorMargin) {
+        this.indicatorMargin = indicatorMargin;
+    }
+
+    public int getIndicatorShowMode() {
+        return indicatorShowMode;
+    }
+
+    /**
+     * the indicator show mode
+     * {@link #INDICATOR_SHOW_WHEN_TOUCH}
+     * {@link #INDICATOR_ALWAYS_SHOW}
+     * {@link #INDICATOR_ALWAYS_SHOW_AFTER_TOUCH}
+     * {@link #INDICATOR_ALWAYS_SHOW}
+     * @param indicatorShowMode
+     */
+    public void setIndicatorShowMode(@IndicatorModeDef int indicatorShowMode) {
+        this.indicatorShowMode = indicatorShowMode;
+    }
+
+    public void showIndicator(boolean isShown) {
+        isShowIndicator = isShown;
+    }
+
+    public boolean isShowIndicator() {
+        return isShowIndicator;
+    }
+
+    /**
+         * include indicator text Height、padding、margin
+         *
+         * @return The actual occupation height of indicator
+         */
+    public int getIndicatorRawHeight() {
+        if (indicatorHeight > 0) {
+            if (indicatorBitmap != null) {
+                return indicatorHeight + indicatorMargin;
+            } else {
+                return indicatorHeight + indicatorArrowSize + indicatorMargin;
+            }
+        } else {
+            if (indicatorBitmap != null) {
+                return Utils.measureText("8", indicatorTextSize).height() + indicatorPaddingTop + indicatorPaddingBottom + indicatorMargin;
+            } else {
+                return Utils.measureText("8", indicatorTextSize).height() + indicatorPaddingTop + indicatorPaddingBottom + indicatorMargin + indicatorArrowSize;
+            }
+        }
+    }
+
+    public int getIndicatorHeight() {
+        return indicatorHeight;
+    }
+
+    public void setIndicatorHeight(int indicatorHeight) {
+        this.indicatorHeight = indicatorHeight;
+    }
+
+    public int getIndicatorWidth() {
+        return indicatorWidth;
+    }
+
+    public void setIndicatorWidth(int indicatorWidth) {
+        this.indicatorWidth = indicatorWidth;
+    }
+
+    public int getIndicatorTextSize() {
+        return indicatorTextSize;
+    }
+
+    public void setIndicatorTextSize(int indicatorTextSize) {
+        this.indicatorTextSize = indicatorTextSize;
+    }
+
+    public int getIndicatorTextColor() {
+        return indicatorTextColor;
+    }
+
+    public void setIndicatorTextColor(@ColorInt int indicatorTextColor) {
+        this.indicatorTextColor = indicatorTextColor;
+    }
+
+    public int getIndicatorBackgroundColor() {
+        return indicatorBackgroundColor;
+    }
+
+    public void setIndicatorBackgroundColor(@ColorInt int indicatorBackgroundColor) {
+        this.indicatorBackgroundColor = indicatorBackgroundColor;
+    }
+
+    public int getThumbInactivatedDrawableId() {
+        return thumbInactivatedDrawableId;
+    }
+
+    public void setThumbInactivatedDrawableId(@DrawableRes int thumbInactivatedDrawableId, int width, int height) {
+        if (thumbInactivatedDrawableId != 0 && getResources() != null) {
+            this.thumbInactivatedDrawableId = thumbInactivatedDrawableId;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                thumbInactivatedBitmap = Utils.drawableToBitmap(width, height, getResources().getDrawable(thumbInactivatedDrawableId, null));
+            } else {
+                thumbInactivatedBitmap = Utils.drawableToBitmap(width, height, getResources().getDrawable(thumbInactivatedDrawableId));
+            }
+        }
+    }
+
+    public int getThumbDrawableId() {
+        return thumbDrawableId;
+    }
+
+    public void setThumbDrawableId(@DrawableRes int thumbDrawableId, int width, int height) {
+        if (thumbDrawableId != 0 && getResources() != null && width > 0 && height > 0) {
+            this.thumbDrawableId = thumbDrawableId;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                thumbBitmap = Utils.drawableToBitmap(width, height, getResources().getDrawable(thumbDrawableId, null));
+            } else {
+                thumbBitmap = Utils.drawableToBitmap(width, height, getResources().getDrawable(thumbDrawableId));
+            }
+        }
+    }
+
+    public void setThumbDrawableId(@DrawableRes int thumbDrawableId) {
+        if (thumbWidth <= 0 || thumbHeight <= 0){
+            throw new IllegalArgumentException("please set thumbWidth and thumbHeight first!");
+        }
+        if (thumbDrawableId != 0 && getResources() != null) {
+            this.thumbDrawableId = thumbDrawableId;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+                thumbBitmap = Utils.drawableToBitmap(thumbWidth, thumbHeight, getResources().getDrawable(thumbDrawableId, null));
+            } else {
+                thumbBitmap = Utils.drawableToBitmap(thumbWidth, thumbHeight, getResources().getDrawable(thumbDrawableId));
+            }
+        }
+    }
+
+    public int getThumbWidth() {
+        return thumbWidth;
+    }
+
+    public void setThumbWidth(int thumbWidth) {
+        this.thumbWidth = thumbWidth;
+    }
+
+    public float getThumbScaleHeight() {
+        return thumbHeight * thumbScaleRatio;
+    }
+
+    public float getThumbScaleWidth() {
+        return thumbWidth * thumbScaleRatio;
+    }
+
+    public int getThumbHeight() {
+        return thumbHeight;
+    }
+
+    public void setThumbHeight(int thumbHeight) {
+        this.thumbHeight = thumbHeight;
+    }
+
+    public float getIndicatorRadius() {
+        return indicatorRadius;
+    }
+
+    public void setIndicatorRadius(float indicatorRadius) {
+        this.indicatorRadius = indicatorRadius;
+    }
+
+    protected boolean getActivate() {
+        return isActivate;
+    }
+
+    protected void setActivate(boolean activate) {
+        isActivate = activate;
+    }
+
+    public void setTypeface(Typeface typeFace) {
+        paint.setTypeface(typeFace);
+    }
+
+
+    /**
+     * when you touch or move, the thumb will scale, default not scale
+     *
+     * @return default 1.0f
+     */
+    public float getThumbScaleRatio() {
+        return thumbScaleRatio;
+    }
+
+    public boolean isVisible() {
+        return isVisible;
+    }
+
+    /**
+     * if visble is false, will clear the Canvas
+     *
+     * @param visible
+     */
+    public void setVisible(boolean visible) {
+        isVisible = visible;
+    }
+
+    public float getProgress() {
+        float range = rangeSeekBar.getMaxProgress() - rangeSeekBar.getMinProgress();
+        return rangeSeekBar.getMinProgress() + range * currPercent;
+    }
+}

+ 21 - 0
RangeSeekBar/src/main/java/com/jaygoo/widget/SeekBarState.java

@@ -0,0 +1,21 @@
+package com.jaygoo.widget;
+
+/**
+ * ================================================
+ * 作    者:JayGoo
+ * 版    本:
+ * 创建日期:2018/5/9
+ * 描    述: it works for draw indicator text
+ * ================================================
+ */
+public class SeekBarState {
+    public String indicatorText;
+    public float value; //now progress value
+    public boolean isMin;
+    public boolean isMax;
+
+    @Override
+    public String toString() {
+        return "indicatorText: " + indicatorText + " ,isMin: " + isMin + " ,isMax: " + isMax;
+    }
+}

+ 186 - 0
RangeSeekBar/src/main/java/com/jaygoo/widget/Utils.java

@@ -0,0 +1,186 @@
+package com.jaygoo.widget;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.NinePatch;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+
+import androidx.annotation.ColorRes;
+import androidx.core.content.ContextCompat;
+
+import android.util.Log;
+
+/**
+ * ================================================
+ * 作    者:JayGoo
+ * 版    本:
+ * 创建日期:2018/5/8
+ * 描    述:
+ * ================================================
+ */
+public class Utils {
+
+    private static final String TAG = "RangeSeekBar";
+
+    public static void print(String log) {
+        Log.d(TAG, log);
+    }
+
+    public static void print(Object... logs) {
+        StringBuilder stringBuilder = new StringBuilder();
+        for (Object log : logs) {
+            stringBuilder.append(log);
+        }
+        Log.d(TAG, stringBuilder.toString());
+    }
+
+    public static Bitmap drawableToBitmap(Context context, int width, int height, int drawableId) {
+        if (context == null || width <= 0 || height <= 0 || drawableId == 0) return null;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            return Utils.drawableToBitmap(width, height, context.getResources().getDrawable(drawableId, null));
+        } else {
+            return Utils.drawableToBitmap(width, height, context.getResources().getDrawable(drawableId));
+        }
+    }
+
+    /**
+     * make a drawable to a bitmap
+     *
+     * @param drawable drawable you want convert
+     * @return converted bitmap
+     */
+    public static Bitmap drawableToBitmap(int width, int height, Drawable drawable) {
+        Bitmap bitmap = null;
+        try {
+            if (drawable instanceof BitmapDrawable) {
+                BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+                bitmap = bitmapDrawable.getBitmap();
+                if (bitmap != null && bitmap.getHeight() > 0) {
+                    Matrix matrix = new Matrix();
+                    float scaleWidth = width * 1.0f / bitmap.getWidth();
+                    float scaleHeight = height * 1.0f / bitmap.getHeight();
+                    matrix.postScale(scaleWidth, scaleHeight);
+                    bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
+                    return bitmap;
+                }
+            }
+            bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+            Canvas canvas = new Canvas(bitmap);
+            drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+            drawable.draw(canvas);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return bitmap;
+    }
+
+    /**
+     * draw 9Path
+     *
+     * @param canvas Canvas
+     * @param bmp    9path bitmap
+     * @param rect   9path rect
+     */
+    public static void drawNinePath(Canvas canvas, Bitmap bmp, Rect rect) {
+        NinePatch.isNinePatchChunk(bmp.getNinePatchChunk());
+        NinePatch patch = new NinePatch(bmp, bmp.getNinePatchChunk(), null);
+        patch.draw(canvas, rect);
+    }
+
+    public static void drawBitmap(Canvas canvas, Paint paint, Bitmap bmp, Rect rect) {
+        try {
+            if (NinePatch.isNinePatchChunk(bmp.getNinePatchChunk())) {
+                drawNinePath(canvas, bmp, rect);
+                return;
+            }
+        } catch (Exception e) {
+        }
+        canvas.drawBitmap(bmp, rect.left, rect.top, paint);
+    }
+
+    public static int dp2px(Context context, float dpValue) {
+        if (context == null || compareFloat(0f, dpValue) == 0) return 0;
+        final float scale = context.getResources().getDisplayMetrics().density;
+        return (int) (dpValue * scale + 0.5f);
+    }
+
+    /**
+     * Compare the size of two floating point numbers
+     *
+     * @param a
+     * @param b
+     * @return 1 is a > b
+     * -1 is a < b
+     * 0 is a == b
+     */
+    public static int compareFloat(float a, float b) {
+        int ta = Math.round(a * 1000000);
+        int tb = Math.round(b * 1000000);
+        if (ta > tb) {
+            return 1;
+        } else if (ta < tb) {
+            return -1;
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Compare the size of two floating point numbers with accuracy
+     *
+     * @param a
+     * @param b
+     * @return 1 is a > b
+     * -1 is a < b
+     * 0 is a == b
+     */
+    public static int compareFloat(float a, float b, int degree) {
+        if (Math.abs(a - b) < Math.pow(0.1, degree)) {
+            return 0;
+        } else {
+            if (a < b) {
+                return -1;
+            } else {
+                return 1;
+            }
+        }
+    }
+
+    public static float parseFloat(String s) {
+        try {
+            return Float.parseFloat(s);
+        } catch (NumberFormatException e) {
+            return 0f;
+        }
+    }
+
+    public static Rect measureText(String text, float textSize) {
+        Paint paint = new Paint();
+        Rect textRect = new Rect();
+        paint.setTextSize(textSize);
+        paint.getTextBounds(text, 0, text.length(), textRect);
+        paint.reset();
+        return textRect;
+    }
+
+    public static boolean verifyBitmap(Bitmap bitmap) {
+        if (bitmap == null || bitmap.isRecycled() || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
+            return false;
+        }
+        return true;
+    }
+
+    public static int getColor(Context context, @ColorRes int colorId) {
+        if (context != null) {
+            return ContextCompat.getColor(context.getApplicationContext(), colorId);
+        }
+        return Color.WHITE;
+    }
+}

+ 278 - 0
RangeSeekBar/src/main/java/com/jaygoo/widget/VerticalRangeSeekBar.java

@@ -0,0 +1,278 @@
+package com.jaygoo.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+
+import androidx.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+
+/**
+ * ================================================
+ * 作    者:JayGoo
+ * 版    本:
+ * 创建日期:2018/5/10
+ * 描    述:
+ * ================================================
+ */
+public class VerticalRangeSeekBar extends RangeSeekBar {
+
+    //text direction of VerticalRangeSeekBar. include indicator and tickMark
+
+    /**
+     * @hide
+     */
+    @IntDef({TEXT_DIRECTION_VERTICAL, TEXT_DIRECTION_HORIZONTAL})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TextDirectionDef {
+    }
+
+    public final static int TEXT_DIRECTION_VERTICAL = 1;
+    public final static int TEXT_DIRECTION_HORIZONTAL = 2;
+
+    //direction of VerticalRangeSeekBar
+
+    /**
+     * @hide
+     */
+    @IntDef({DIRECTION_LEFT, DIRECTION_RIGHT})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DirectionDef {
+    }
+
+    public final static int DIRECTION_LEFT = 1;
+    public final static int DIRECTION_RIGHT = 2;
+
+    private int orientation = DIRECTION_LEFT;
+    private int tickMarkDirection = TEXT_DIRECTION_VERTICAL;
+
+    private int maxTickMarkWidth;
+
+    public VerticalRangeSeekBar(Context context) {
+        this(context, null);
+    }
+
+    public VerticalRangeSeekBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initAttrs(attrs);
+        initSeekBar(attrs);
+    }
+
+    private void initAttrs(AttributeSet attrs) {
+        try {
+            TypedArray t = getContext().obtainStyledAttributes(attrs, R.styleable.VerticalRangeSeekBar);
+            orientation = t.getInt(R.styleable.VerticalRangeSeekBar_rsb_orientation, DIRECTION_LEFT);
+            tickMarkDirection = t.getInt(R.styleable.VerticalRangeSeekBar_rsb_tick_mark_orientation, TEXT_DIRECTION_VERTICAL);
+            t.recycle();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+
+    protected void initSeekBar(AttributeSet attrs) {
+        leftSB = new VerticalSeekBar(this, attrs, true);
+        rightSB = new VerticalSeekBar(this, attrs, false);
+        rightSB.setVisible(getSeekBarMode() != SEEKBAR_MODE_SINGLE);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(h, w, oldh, oldw);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        /*
+         * onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值
+         * MeasureSpec.EXACTLY 是精确尺寸
+         * MeasureSpec.AT_MOST 是最大尺寸
+         * MeasureSpec.UNSPECIFIED 是未指定尺寸
+         */
+
+        if (widthMode == MeasureSpec.EXACTLY) {
+            widthSize = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+        } else if (widthMode == MeasureSpec.AT_MOST && getParent() instanceof ViewGroup
+                && widthSize == ViewGroup.LayoutParams.MATCH_PARENT) {
+            widthSize = MeasureSpec.makeMeasureSpec(((ViewGroup) getParent()).getMeasuredHeight(), MeasureSpec.AT_MOST);
+        } else {
+            int heightNeeded;
+            if (getGravity() == Gravity.CENTER) {
+                heightNeeded = 2 * getProgressTop() + getProgressHeight();
+            } else {
+                heightNeeded = (int) getRawHeight();
+            }
+            widthSize = MeasureSpec.makeMeasureSpec(heightNeeded, MeasureSpec.EXACTLY);
+        }
+        super.onMeasure(widthSize, heightMeasureSpec);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (orientation == DIRECTION_LEFT) {
+            canvas.rotate(-90);
+            canvas.translate(-getHeight(), 0);
+        } else {
+            canvas.rotate(90);
+            canvas.translate(0, -getWidth());
+        }
+        super.onDraw(canvas);
+    }
+
+    @Override
+    protected void onDrawTickMark(Canvas canvas, Paint paint) {
+        if (getTickMarkTextArray() != null) {
+            int arrayLength = getTickMarkTextArray().length;
+            int trickPartWidth = getProgressWidth() / (arrayLength - 1);
+            for (int i = 0; i < arrayLength; i++) {
+                final String text2Draw = getTickMarkTextArray()[i].toString();
+                if (TextUtils.isEmpty(text2Draw)) continue;
+                paint.getTextBounds(text2Draw, 0, text2Draw.length(), tickMarkTextRect);
+                paint.setColor(getTickMarkTextColor());
+                //平分显示
+                float x;
+                if (getTickMarkMode() == TRICK_MARK_MODE_OTHER) {
+                    if (getTickMarkGravity() == TICK_MARK_GRAVITY_RIGHT) {
+                        x = getProgressLeft() + i * trickPartWidth - tickMarkTextRect.width();
+                    } else if (getTickMarkGravity() == TICK_MARK_GRAVITY_CENTER) {
+                        x = getProgressLeft() + i * trickPartWidth - tickMarkTextRect.width() / 2f;
+                    } else {
+                        x = getProgressLeft() + i * trickPartWidth;
+                    }
+                } else {
+                    float num = Utils.parseFloat(text2Draw);
+                    SeekBarState[] states = getRangeSeekBarState();
+                    if (Utils.compareFloat(num, states[0].value) != -1 && Utils.compareFloat(num, states[1].value) != 1 && (getSeekBarMode() == SEEKBAR_MODE_RANGE)) {
+                        paint.setColor(getTickMarkInRangeTextColor());
+                    }
+                    //按实际比例显示
+                    x = getProgressLeft() + getProgressWidth() * (num - getMinProgress()) / (getMaxProgress() - getMinProgress())
+                            - tickMarkTextRect.width() / 2f;
+                }
+                float y;
+                if (getTickMarkLayoutGravity() == Gravity.TOP) {
+                    y = getProgressTop() - getTickMarkTextMargin();
+                } else {
+                    y = getProgressBottom() + getTickMarkTextMargin() + tickMarkTextRect.height();
+                }
+                int degrees = 0;
+                float rotateX = (x + tickMarkTextRect.width() / 2f);
+                float rotateY = (y - tickMarkTextRect.height() / 2f);
+                if (tickMarkDirection == TEXT_DIRECTION_VERTICAL) {
+                    if (orientation == DIRECTION_LEFT) {
+                        degrees = 90;
+                    } else if (orientation == DIRECTION_RIGHT) {
+                        degrees = -90;
+                    }
+                }
+                if (degrees != 0) {
+                    canvas.rotate(degrees, rotateX, rotateY);
+                }
+                canvas.drawText(text2Draw, x, y, paint);
+                if (degrees != 0) {
+                    canvas.rotate(-degrees, rotateX, rotateY);
+                }
+            }
+        }
+
+    }
+
+
+    @Override
+    protected int getTickMarkRawHeight() {
+        if (maxTickMarkWidth > 0) return getTickMarkTextMargin() + maxTickMarkWidth;
+        if (getTickMarkTextArray() != null && getTickMarkTextArray().length > 0) {
+            int arrayLength = getTickMarkTextArray().length;
+            maxTickMarkWidth = Utils.measureText(String.valueOf(getTickMarkTextArray()[0]), getTickMarkTextSize()).width();
+            for (int i = 1; i < arrayLength; i++) {
+                int width = Utils.measureText(String.valueOf(getTickMarkTextArray()[i]), getTickMarkTextSize()).width();
+                if (maxTickMarkWidth < width) {
+                    maxTickMarkWidth = width;
+                }
+            }
+            return getTickMarkTextMargin() + maxTickMarkWidth;
+        }
+        return 0;
+    }
+
+    @Override
+    public void setTickMarkTextSize(int tickMarkTextSize) {
+        super.setTickMarkTextSize(tickMarkTextSize);
+        maxTickMarkWidth = 0;
+    }
+
+    @Override
+    public void setTickMarkTextArray(CharSequence[] tickMarkTextArray) {
+        super.setTickMarkTextArray(tickMarkTextArray);
+        maxTickMarkWidth = 0;
+    }
+
+    @Override
+    protected float getEventX(MotionEvent event) {
+        if (orientation == DIRECTION_LEFT) {
+            return getHeight() - event.getY();
+        } else {
+            return event.getY();
+        }
+    }
+
+    @Override
+    protected float getEventY(MotionEvent event) {
+        if (orientation == DIRECTION_LEFT) {
+            return event.getX();
+        } else {
+            return -event.getX() + getWidth();
+        }
+    }
+
+    /**
+     * if is single mode, please use it to get the SeekBar
+     *
+     * @return left seek bar
+     */
+    public VerticalSeekBar getLeftSeekBar() {
+        return (VerticalSeekBar) leftSB;
+    }
+
+    public VerticalSeekBar getRightSeekBar() {
+        return (VerticalSeekBar) rightSB;
+    }
+
+    public int getOrientation() {
+        return orientation;
+    }
+
+    /**
+     * set VerticalRangeSeekBar Orientation
+     * {@link #DIRECTION_LEFT}
+     * {@link #DIRECTION_RIGHT}
+     * @param orientation
+     */
+    public void setOrientation(@DirectionDef int orientation) {
+        this.orientation = orientation;
+    }
+
+    public int getTickMarkDirection() {
+        return tickMarkDirection;
+    }
+
+    /**
+     * set tick mark text direction
+     * {@link #TEXT_DIRECTION_VERTICAL}
+     * {@link #TEXT_DIRECTION_HORIZONTAL}
+     * @param tickMarkDirection
+     */
+    public void setTickMarkDirection(@TextDirectionDef int tickMarkDirection) {
+        this.tickMarkDirection = tickMarkDirection;
+    }
+}

+ 170 - 0
RangeSeekBar/src/main/java/com/jaygoo/widget/VerticalSeekBar.java

@@ -0,0 +1,170 @@
+package com.jaygoo.widget;
+
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+
+import static com.jaygoo.widget.VerticalRangeSeekBar.DIRECTION_LEFT;
+import static com.jaygoo.widget.VerticalRangeSeekBar.DIRECTION_RIGHT;
+import static com.jaygoo.widget.VerticalRangeSeekBar.TEXT_DIRECTION_VERTICAL;
+
+
+/**
+ * //                       _ooOoo_
+ * //                      o8888888o
+ * //                      88" . "88
+ * //                      (| -_- |)
+ * //                       O\ = /O
+ * //                   ____/`---'\____
+ * //                 .   ' \\| |// `.
+ * //                  / \\||| : |||// \
+ * //                / _||||| -:- |||||- \
+ * //                  | | \\\ - /// | |
+ * //                | \_| ''\---/'' | |
+ * //                 \ .-\__ `-` ___/-. /
+ * //              ______`. .' /--.--\ `. . __
+ * //           ."" '< `.___\_<|>_/___.' >'"".
+ * //          | | : `- \`.;`\ _ /`;.`/ - ` : | |
+ * //            \ \ `-. \_ __\ /__ _/ .-` / /
+ * //    ======`-.____`-.___\_____/___.-`____.-'======
+ * //                       `=---='
+ * //
+ * //    .............................................
+ * //             佛祖保佑             永无BUG
+ * =====================================================
+ * 作    者:JayGoo
+ * 创建日期:2019-06-05
+ * 描    述:
+ * =====================================================
+ */
+public class VerticalSeekBar extends SeekBar {
+
+    private int indicatorTextOrientation;
+    VerticalRangeSeekBar verticalSeekBar;
+
+    public VerticalSeekBar(RangeSeekBar rangeSeekBar, AttributeSet attrs, boolean isLeft) {
+        super(rangeSeekBar, attrs, isLeft);
+        initAttrs(attrs);
+        verticalSeekBar = (VerticalRangeSeekBar) rangeSeekBar;
+    }
+
+    private void initAttrs(AttributeSet attrs) {
+        try {
+            TypedArray t = getContext().obtainStyledAttributes(attrs, R.styleable.VerticalRangeSeekBar);
+            indicatorTextOrientation = t.getInt(R.styleable.VerticalRangeSeekBar_rsb_indicator_text_orientation, TEXT_DIRECTION_VERTICAL);
+            t.recycle();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    protected void onDrawIndicator(Canvas canvas, Paint paint, String text2Draw) {
+        if (text2Draw == null) return;
+        //draw indicator
+        if (indicatorTextOrientation == TEXT_DIRECTION_VERTICAL) {
+            drawVerticalIndicator(canvas, paint, text2Draw);
+        } else {
+            super.onDrawIndicator(canvas, paint, text2Draw);
+        }
+    }
+
+    protected void drawVerticalIndicator(Canvas canvas, Paint paint, String text2Draw) {
+        //measure indicator text
+        paint.setTextSize(getIndicatorTextSize());
+        paint.setStyle(Paint.Style.FILL);
+        paint.setColor(getIndicatorBackgroundColor());
+        paint.getTextBounds(text2Draw, 0, text2Draw.length(), indicatorTextRect);
+
+        int realIndicatorWidth = indicatorTextRect.height() + getIndicatorPaddingLeft() + getIndicatorPaddingRight();
+        if (getIndicatorWidth() > realIndicatorWidth) {
+            realIndicatorWidth = getIndicatorWidth();
+        }
+
+        int realIndicatorHeight = indicatorTextRect.width() + getIndicatorPaddingTop() + getIndicatorPaddingBottom();
+        if (getIndicatorHeight() > realIndicatorHeight) {
+            realIndicatorHeight = getIndicatorHeight();
+        }
+
+        indicatorRect.left = scaleThumbWidth / 2 - realIndicatorWidth / 2;
+        indicatorRect.top = bottom - realIndicatorHeight - scaleThumbHeight - getIndicatorMargin();
+        indicatorRect.right = indicatorRect.left + realIndicatorWidth;
+        indicatorRect.bottom = indicatorRect.top + realIndicatorHeight;
+
+        //draw default indicator arrow
+        if (indicatorBitmap == null) {
+            //arrow three point
+            //  b   c
+            //    a
+            int ax = scaleThumbWidth / 2;
+            int ay = indicatorRect.bottom;
+            int bx = ax - getIndicatorArrowSize();
+            int by = ay - getIndicatorArrowSize();
+            int cx = ax + getIndicatorArrowSize();
+            indicatorArrowPath.reset();
+            indicatorArrowPath.moveTo(ax, ay);
+            indicatorArrowPath.lineTo(bx, by);
+            indicatorArrowPath.lineTo(cx, by);
+            indicatorArrowPath.close();
+            canvas.drawPath(indicatorArrowPath, paint);
+            indicatorRect.bottom -= getIndicatorArrowSize();
+            indicatorRect.top -= getIndicatorArrowSize();
+        }
+
+        int defaultPaddingOffset = Utils.dp2px(getContext(), 1);
+        int leftOffset = indicatorRect.width() / 2 - (int) (rangeSeekBar.getProgressWidth() * currPercent) - rangeSeekBar.getProgressLeft() + defaultPaddingOffset;
+        int rightOffset = indicatorRect.width() / 2 - (int) (rangeSeekBar.getProgressWidth() * (1 - currPercent)) - rangeSeekBar.getProgressPaddingRight() + defaultPaddingOffset;
+        if (leftOffset > 0) {
+            indicatorRect.left += leftOffset;
+            indicatorRect.right += leftOffset;
+        } else if (rightOffset > 0) {
+            indicatorRect.left -= rightOffset;
+            indicatorRect.right -= rightOffset;
+        }
+
+        //draw indicator background
+        if (indicatorBitmap != null) {
+            Utils.drawBitmap(canvas, paint, indicatorBitmap, indicatorRect);
+        } else if (getIndicatorRadius() > 0f) {
+            canvas.drawRoundRect(new RectF(indicatorRect), getIndicatorRadius(), getIndicatorRadius(), paint);
+        } else {
+            canvas.drawRect(indicatorRect, paint);
+        }
+
+        //draw indicator content text
+        int tx = indicatorRect.left + (indicatorRect.width() - indicatorTextRect.width()) / 2 + getIndicatorPaddingLeft() - getIndicatorPaddingRight();
+        int ty = indicatorRect.bottom - (indicatorRect.height() - indicatorTextRect.height()) / 2 + getIndicatorPaddingTop() - getIndicatorPaddingBottom();
+
+        //draw indicator text
+        paint.setColor(getIndicatorTextColor());
+
+        int degrees = 0;
+        float rotateX = (tx + indicatorTextRect.width() / 2f);
+        float rotateY = (ty - indicatorTextRect.height() / 2f);
+
+        if (indicatorTextOrientation == TEXT_DIRECTION_VERTICAL) {
+            if (verticalSeekBar.getOrientation() == DIRECTION_LEFT) {
+                degrees = 90;
+            } else if (verticalSeekBar.getOrientation() == DIRECTION_RIGHT) {
+                degrees = -90;
+            }
+        }
+        if (degrees != 0) {
+            canvas.rotate(degrees, rotateX, rotateY);
+        }
+        canvas.drawText(text2Draw, tx, ty, paint);
+        if (degrees != 0) {
+            canvas.rotate(-degrees, rotateX, rotateY);
+        }
+    }
+
+    public int getIndicatorTextOrientation() {
+        return indicatorTextOrientation;
+    }
+
+    public void setIndicatorTextOrientation(@VerticalRangeSeekBar.TextDirectionDef int orientation) {
+        this.indicatorTextOrientation = orientation;
+    }
+}

+ 8 - 0
RangeSeekBar/src/main/res/drawable/rsb_default_thumb.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape android:shape="oval"
+       xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@color/rsbColorThumbDefault"/>
+    <stroke android:width="1dp"
+            android:color="@color/rsbColorThumbBorder"/>
+</shape>
+

+ 141 - 0
RangeSeekBar/src/main/res/values/attrs.xml

@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <declare-styleable name="RangeSeekBar">
+        <!--RangeSeekBar common attrs-->
+
+        <!--The maximum-->
+        <attr name="rsb_max" format="float"/>
+
+        <!--The minimum-->
+        <attr name="rsb_min" format="float"/>
+
+        <!--RangeSeekBar mode, single is normal seekBar, range is RangeSeekBar-->
+        <attr name="rsb_mode" format="enum">
+            <enum name="single" value="1"/>
+            <enum name="range" value="2"/>
+        </attr>
+
+        <!--RangeSeekBar gravity-->
+        <attr name="rsb_gravity" format="enum">
+            <enum name="top" value="0"/>
+            <enum name="bottom" value="1"/>
+            <enum name="center" value="2"/>
+        </attr>
+
+        <!--The min interval of the thumbs -->
+        <attr name="rsb_min_interval" format="float"/>
+
+        <!-- 0 for the normal mode, greater than 1 to switch to scale mode-->
+        <attr name="rsb_tick_mark_number" format="integer"/>
+
+        <!--Scale mode
+        Number according to the scale of the actual proportion of the distribution of the location (markTextArray must be a number)
+        Other bisects the current layout (markTextArray can be any character)
+        -->
+        <attr name="rsb_tick_mark_mode" format="enum">
+            <enum name="number" value="0"/>
+            <enum name="other" value="1"/>
+        </attr>
+
+        <!--The tick mark text gravity -->
+        <attr name="rsb_tick_mark_gravity" format="enum">
+            <enum name="left" value="0"/>
+            <enum name="center" value="1"/>
+            <enum name="right" value="2"/>
+        </attr>
+
+        <!--The tick mark text layout gravity -->
+        <attr name="rsb_tick_mark_layout_gravity" format="enum">
+            <enum name="top" value="0"/>
+            <enum name="bottom" value="1"/>
+        </attr>
+
+        <!--The tick mark text array -->
+        <attr name="rsb_tick_mark_text_array" format="reference"/>
+
+        <!--The tick mark text margin bottom to progress -->
+        <attr name="rsb_tick_mark_text_margin" format="dimension" />
+
+        <attr name="rsb_tick_mark_text_size" format="dimension" />
+
+        <attr name="rsb_tick_mark_text_color" format="color" />
+
+        <!--it just work in range && number mode now-->
+        <attr name="rsb_tick_mark_in_range_text_color" format="color" />
+
+        <attr name="rsb_progress_height" format="dimension"/>
+
+        <attr name="rsb_progress_radius" format="dimension"/>
+
+        <!--the color of progress bar when in progress-->
+        <attr name="rsb_progress_color" format="color"/>
+        <!--the default color of the progress bar-->
+        <attr name="rsb_progress_default_color" format="color"/>
+
+        <attr name="rsb_progress_drawable" format="reference"/>
+
+        <attr name="rsb_progress_drawable_default" format="reference"/>
+
+        <!--SeekBar attrs-->
+        <attr name="rsb_indicator_show_mode" format="enum">
+            <enum name="showWhenTouch" value="0"/>
+            <enum name="alwaysHide" value="1"/>
+            <enum name="alwaysShowAfterTouch" value="2"/>
+            <enum name="alwaysShow" value="3"/>
+        </attr>
+        <attr name="rsb_indicator_height" format="dimension" >
+            <enum name="wrap_content" value="-1" />
+        </attr>
+        <attr name="rsb_indicator_width" format="dimension" >
+            <enum name="wrap_content" value="-1" />
+        </attr>
+        <!--indicator margin bottom to progress bar-->
+        <attr name="rsb_indicator_margin" format="dimension" />
+        <attr name="rsb_indicator_text_size" format="dimension" />
+        <attr name="rsb_indicator_text_color" format="color" />
+        <!--indicator arrow size, it just work when you not use rsb_indicator_drawable -->
+        <attr name="rsb_indicator_arrow_size" format="dimension" />
+        <!-- must use 9 path !!!-->
+        <attr name="rsb_indicator_drawable" format="reference"/>
+        <attr name="rsb_indicator_background_color" format="color" />
+        <attr name="rsb_indicator_padding_left" format="dimension" />
+        <attr name="rsb_indicator_padding_right" format="dimension" />
+        <attr name="rsb_indicator_padding_top" format="dimension" />
+        <attr name="rsb_indicator_padding_bottom" format="dimension" />
+        <attr name="rsb_indicator_radius" format="dimension" />
+        <attr name="rsb_thumb_drawable" format="reference"/>
+        <!--the thumb inactivated is when you don't touch the thumb button-->
+        <attr name="rsb_thumb_inactivated_drawable" format="reference"/>
+        <attr name="rsb_thumb_width" format="dimension"/>
+        <attr name="rsb_thumb_height" format="dimension"/>
+        <attr name="rsb_thumb_scale_ratio" format="float"/>
+
+        <!--steps SeekBar-->
+        <attr name="rsb_steps" format="integer"/>
+        <attr name="rsb_step_color" format="color"/>
+        <attr name="rsb_step_width" format="dimension"/>
+        <attr name="rsb_step_height" format="dimension"/>
+        <attr name="rsb_step_radius" format="dimension"/>
+        <attr name="rsb_step_auto_bonding" format="boolean"/>
+        <attr name="rsb_step_drawable" format="reference"/>
+
+    </declare-styleable>
+
+    <declare-styleable name="VerticalRangeSeekBar" >
+        <!--the vertical RangeSeekBar draw orientation-->
+        <attr name="rsb_orientation" format="enum">
+            <enum name="left" value="1"/>
+            <enum name="right" value="2"/>
+        </attr>
+        <attr name="rsb_tick_mark_orientation" format="enum">
+            <enum name="vertical" value="1"/>
+            <enum name="horizontal" value="2"/>
+        </attr>
+        <attr name="rsb_indicator_text_orientation" format="enum">
+            <enum name="vertical" value="1"/>
+            <enum name="horizontal" value="2"/>
+        </attr>
+    </declare-styleable>
+
+
+</resources>

+ 10 - 0
RangeSeekBar/src/main/res/values/colors.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#3F51B5</color>
+    <color name="colorPrimaryDark">#303F9F</color>
+    <color name="colorAccent">#2ed184</color>
+    <color name="rsbColorSeekBarDefault">#c3c3c3</color>
+    <color name="rsbColorThumbPressed">#FFE7E7E7</color>
+    <color name="rsbColorThumbDefault">#FFFFFF</color>
+    <color name="rsbColorThumbBorder">#FFD7D7D7</color>
+</resources>

+ 3 - 0
RangeSeekBar/src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">Library</string>
+</resources>

+ 11 - 0
RangeSeekBar/src/main/res/values/styles.xml

@@ -0,0 +1,11 @@
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+</resources>

+ 17 - 0
RangeSeekBar/src/test/java/com/jaygoo/widget/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.jaygoo.widget;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() throws Exception {
+        assertEquals(4, 2 + 2);
+    }
+}

+ 1 - 0
app/.gitignore

@@ -0,0 +1 @@
+/build

+ 157 - 0
app/build.gradle

@@ -0,0 +1,157 @@
+plugins {
+    id 'com.android.application'
+    id 'org.jetbrains.kotlin.android'
+    id 'kotlin-kapt'
+    id 'com.google.dagger.hilt.android'
+    id 'kotlinx-serialization'
+}
+
+android {
+    compileSdk 32
+
+    defaultConfig {
+        applicationId "com.rdiot.yx485"
+        minSdk 23
+        targetSdk 32
+        versionCode 3
+        versionName "1.0.3"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    signingConfigs {
+        release {
+            storeFile file('yx485.jks')
+            storePassword "yx485123456"
+            keyAlias 'yx485'
+            keyPassword "yx485123456"
+        }
+    }
+
+    buildTypes {
+        debug {
+            signingConfig signingConfigs.release
+        }
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+            signingConfig signingConfigs.release
+
+            applicationVariants.all { variant ->
+                variant.outputs.all {
+                    println(variant)
+                    outputFileName = "RD-YX485-${buildType.name}-${defaultConfig.versionName}-${releaseTime()}.apk"
+                }
+            }
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+    kotlinOptions {
+        jvmTarget = '1.8'
+    }
+
+    dataBinding {
+        enabled true
+    }
+}
+
+
+dependencies {
+
+    implementation 'androidx.core:core-ktx:1.7.0'
+    implementation 'androidx.appcompat:appcompat:1.5.1'
+    implementation 'com.google.android.material:material:1.6.1'
+    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+    testImplementation 'junit:junit:4.13.2'
+    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
+    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"
+    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
+
+    // ViewModel
+    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
+    implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
+    implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1"
+    implementation "androidx.activity:activity-ktx:1.5.1"
+    implementation "androidx.fragment:fragment-ktx:1.3.0"
+
+    // Hilt
+    implementation "com.google.dagger:hilt-android:2.44"
+    kapt "com.google.dagger:hilt-compiler:2.44"
+
+
+    // navigation
+    implementation("androidx.navigation:navigation-fragment-ktx:2.5.3")
+    implementation("androidx.navigation:navigation-ui-ktx:2.5.3")
+
+    // 状态栏 https://github.com/gyf-dev/ImmersionBar
+    // 基础依赖包,必须要依赖
+    implementation 'com.geyifeng.immersionbar:immersionbar:3.2.2'
+    // kotlin扩展(可选)
+    implementation 'com.geyifeng.immersionbar:immersionbar-ktx:3.2.2'
+
+    // PageView https://github.com/ckrgithub/PageRecyclerView/blob/master/README-ZH.md
+    implementation project(path: ':pageview')
+
+    // 弧形控制 https://github.com/jenly1314/ArcSeekBar
+    implementation project(path: ':arcseekbar')
+
+    // net https://liangjingkanji.github.io/Net/
+    implementation 'com.squareup.okhttp3:okhttp:4.10.0' // 要求OkHttp4以上
+    implementation 'com.github.liangjingkanji:Net:3.5.3'
+    implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2" // JSON序列化库
+    implementation 'com.google.code.gson:gson:2.9.0' // JSON序列化库, 会导致kotlin默认值无效, 故不推荐
+    implementation 'com.github.liangjingkanji:Tooltip:1.2.2'
+    // BRV https://github.com/liangjingkanji/BRV
+    implementation 'com.github.liangjingkanji:BRV:1.3.88'
+    implementation 'com.google.android.flexbox:flexbox:3.0.0'// flexbox伸缩(流式)布局
+
+    //日志记录器
+    implementation 'com.localebro:okhttpprofiler:1.0.8'
+
+    //Serialize https://github.com/liangjingkanji/Serialize
+    implementation 'com.github.liangjingkanji:Serialize:1.3.2'
+
+    //分段SeekBar https://github.com/Jay-Goo/RangeSeekBar
+    implementation project(path: ':RangeSeekBar')
+
+    //BLE库 https://github.com/Jasonchenlijian/FastBle
+    implementation project(path: ':FastBleLib')
+
+    //Permission X https://github.com/guolindev/PermissionX
+    implementation 'com.guolindev.permissionx:permissionx:1.7.1'
+
+    //LiveEventBus https://github.com/JeremyLiao/LiveEventBus
+    implementation 'io.github.jeremyliao:live-event-bus-x:1.8.0'
+
+    //Dialog https://github.com/lindroy/iOSDialog
+    implementation 'com.github.lindroy:iOSDialog:1.0.0'
+
+    //ZXingLite https://github.com/jenly1314/ZXingLite
+    implementation 'com.github.jenly1314:zxing-lite:2.2.1'
+
+    //Glide  https://muyangmin.github.io/glide-docs-cn/
+    implementation("com.github.bumptech.glide:glide:4.12.0") {
+        exclude group: "com.android.support"
+    }
+
+    //picker https://github.com/gzu-liyujiang/AndroidPicker
+    implementation 'com.github.gzu-liyujiang.AndroidPicker:Common:4.1.11'
+    implementation 'com.github.gzu-liyujiang.AndroidPicker:ImagePicker:4.1.11'
+    implementation 'com.github.gzu-liyujiang.AndroidPicker:WheelPicker:4.1.11'
+
+    implementation 'com.github.JessYanCoding:AndroidAutoSize:v1.2.1'
+
+}
+
+// Allow references to generated code
+kapt {
+    correctErrorTypes true
+}
+
+static def releaseTime() {
+    return new Date().format("MM-dd_HHmmss", TimeZone.getTimeZone("Asia/Shanghai"))
+}

+ 21 - 0
app/proguard-rules.pro

@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile

+ 24 - 0
app/src/androidTest/java/com/rdiot/yx485/ExampleInstrumentedTest.kt

@@ -0,0 +1,24 @@
+package com.rdiot.yx485
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+    @Test
+    fun useAppContext() {
+        // Context of the app under test.
+        val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+        assertEquals("com.rdiot.yx485", appContext.packageName)
+    }
+}

+ 127 - 0
app/src/main/AndroidManifest.xml

@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.rdiot.yx485">
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+
+    <uses-permission
+        android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
+        tools:ignore="ScopedStorage" />
+
+    <!-- Request legacy Bluetooth permissions on older devices. -->
+    <uses-permission
+        android:name="android.permission.BLUETOOTH"
+        android:maxSdkVersion="30" />
+    <uses-permission
+        android:name="android.permission.BLUETOOTH_ADMIN"
+        android:maxSdkVersion="30" />
+    <!-- Android12 的蓝牙权限 如果您的应用查找蓝牙设备(如蓝牙低功耗 (BLE) 外围设备) -->
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
+    <!-- Android12 的蓝牙权限 如果您的应用使当前设备可被其他蓝牙设备检测到 -->
+    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+    <!-- Android12 的蓝牙权限 如果您的应用与已配对的蓝牙设备通信或者获取当前手机蓝牙是否打开 -->
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+
+    <uses-permission android:name="android.permission.CAMERA" />
+
+    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+
+
+    <queries>
+        <intent>
+            <action android:name="android.intent.action.PICK" />
+            <data android:mimeType="image/*" />
+        </intent>
+    </queries>
+
+    <application
+        android:name=".MyApp"
+        android:allowBackup="true"
+        android:dataExtractionRules="@xml/data_extraction_rules"
+        android:fullBackupContent="@xml/backup_rules"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme"
+        tools:targetApi="31">
+        <activity
+            android:name="com.rdiot.yx485.ui.main.MainActivity"
+            android:screenOrientation="portrait" />
+
+        <activity
+            android:name=".ui.login.LoginActivity"
+            android:launchMode="singleTop"
+            android:screenOrientation="portrait" />
+
+        <activity
+            android:name=".ui.bind.BindActivity"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".ui.bind.QRCodeActivity"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".ui.mine.UserInfoActivity"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".ui.login.SetPwdActivity"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".ui.family.FamilyManagementActivity"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".ui.web.WebActivity"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".ui.SplashActivity"
+            android:exported="true"
+            android:screenOrientation="portrait">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".ui.mine.SettingActivity"
+            android:screenOrientation="portrait" />
+        <activity
+            android:name=".ui.ctrl.RoomCtrlMainActivity"
+            android:screenOrientation="portrait" />
+
+
+        <!--适配华为(huawei)刘海屏-->
+        <meta-data
+            android:name="android.notch_support"
+            android:value="true" />
+        <!--适配小米(xiaomi)刘海屏-->
+        <meta-data
+            android:name="notch.config"
+            android:value="portrait|landscape" />
+
+        <meta-data
+            android:name="design_width_in_dp"
+            android:value="390" />
+        <meta-data
+            android:name="design_height_in_dp"
+            android:value="844" />
+
+        <provider
+            android:name="androidx.core.content.FileProvider"
+            android:authorities="${applicationId}.fileprovider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/update_cache_path" />
+        </provider>
+    </application>
+
+</manifest>

+ 156 - 0
app/src/main/java/com/rdiot/yx485/MyApp.kt

@@ -0,0 +1,156 @@
+package com.rdiot.yx485
+
+import android.app.Application
+import android.view.View
+import com.clj.fastble.BleManager
+import com.drake.brv.utils.BRV
+import com.drake.net.NetConfig
+import com.drake.net.cookie.PersistentCookieJar
+import com.drake.net.interceptor.LogRecordInterceptor
+import com.drake.net.interfaces.NetErrorHandler
+import com.drake.net.okhttp.setConverter
+import com.drake.net.okhttp.setDebug
+import com.drake.net.okhttp.setDialogFactory
+import com.drake.net.okhttp.setErrorHandler
+import com.drake.net.okhttp.setRequestInterceptor
+import com.drake.net.utils.TipUtils
+import com.drake.net.utils.scopeNet
+import com.drake.tooltip.dialog.BubbleDialog
+import com.jeremyliao.liveeventbus.LiveEventBus
+import com.king.zxing.util.LogUtils
+import com.lindroy.iosdialog.IDialog
+import com.rdiot.yx485.base.ApiHost
+import com.rdiot.yx485.base.LocalData
+import com.rdiot.yx485.net.MyRequestInterceptor
+import com.rdiot.yx485.net.SerializationConverter
+import com.rdiot.yx485.net.appInit
+import com.rdiot.yx485.net.checkVersion
+import com.rdiot.yx485.ui.login.LoginActivity
+import com.scwang.smart.refresh.footer.ClassicsFooter
+import com.scwang.smart.refresh.header.MaterialHeader
+import com.scwang.smart.refresh.layout.SmartRefreshLayout
+import com.tencent.mmkv.MMKV
+import com.tencent.mmkv.MMKVLogLevel
+import dagger.hilt.android.HiltAndroidApp
+import okhttp3.Cache
+import java.util.concurrent.TimeUnit
+
+const val TAG = "YongXu485"
+
+/**
+ *
+ * @author mR2hao
+ * @date 2022/11/10
+ */
+@HiltAndroidApp
+class MyApp : Application() {
+
+    companion object {
+        lateinit var instance: MyApp
+    }
+
+    init {
+        instance = this
+    }
+
+    override fun onCreate() {
+        super.onCreate()
+
+        // 初始化网络
+        initNet()
+        initBRV()
+        // MMKV初始化
+        MMKV.initialize(
+            this,
+            if (BuildConfig.DEBUG) MMKVLogLevel.LevelDebug else MMKVLogLevel.LevelNone
+        )
+
+        // 初始化蓝牙库
+        BleManager.getInstance().init(this)
+        // 初始化Dialog
+        IDialog.init(this)
+
+
+    }
+
+
+    private fun initBRV() {
+        BRV.modelId = BR.vm
+
+        SmartRefreshLayout.setDefaultRefreshHeaderCreator { context, _ ->
+            MaterialHeader(context)
+        }
+        SmartRefreshLayout.setDefaultRefreshFooterCreator { context, _ ->
+            ClassicsFooter(context)
+        }
+    }
+
+    private fun initNet() {
+        val apiHost = LocalData.apiHost
+        if (apiHost == ApiHost.TEST_HOST) {
+            TipUtils.toast("注意,当前为${apiHost.hostName}")
+        }
+
+        NetConfig.initialize(apiHost.host, this) {
+
+            // 超时设置
+            connectTimeout(30, TimeUnit.SECONDS)
+            readTimeout(30, TimeUnit.SECONDS)
+            writeTimeout(30, TimeUnit.SECONDS)
+            // 本框架支持Http缓存协议和强制缓存模式
+            cache(Cache(cacheDir, 1024 * 1024 * 128)) // 缓存设置, 当超过maxSize最大值会根据最近最少使用算法清除缓存来限制缓存大小
+            // LogCat是否输出异常日志, 异常日志可以快速定位网络请求错误
+            setDebug(BuildConfig.DEBUG)
+            // AndroidStudio OkHttp Profiler 插件输出网络日志
+            addInterceptor(LogRecordInterceptor(BuildConfig.DEBUG))
+            // 添加持久化Cookie管理
+            cookieJar(PersistentCookieJar(this@MyApp))
+            setRequestInterceptor(MyRequestInterceptor())
+            // 数据转换器
+            setConverter(SerializationConverter())
+            setDialogFactory { // 全局加载对话框
+                BubbleDialog(it)
+            }
+        }
+
+        NetConfig.initialize(apiHost.host, this) {
+            setErrorHandler(object : NetErrorHandler {
+                override fun onError(e: Throwable) {
+                    super.onError(e)
+                    LogUtils.e("onError: ${e.message}")
+
+                    if (e.message == "令牌失效") {
+                        LiveEventBus.get(LoginActivity.TOKEN_INVALID, String::class.java)
+                            .post(LoginActivity.TOKEN_INVALID)
+                    } else if (e.message?.contains("failed to connect to") == true) {
+                        TipUtils.toast("连接服务器失败")
+                    } else {
+                        TipUtils.toast(e.message)
+                    }
+                }
+
+                override fun onStateError(e: Throwable, view: View) {
+                    super.onStateError(e, view)
+                    LogUtils.e("onStateError: ${e.message}")
+                }
+            })
+        }
+
+        scopeNet {
+            appInit().await().apply { LogUtils.d("网络接口初始化成功") }
+
+            scopeNet {
+                checkVersion().await().apply {
+                    LocalData.appVersionData.postValue(this)
+                }
+            }.catch { e ->
+                LogUtils.e("获取版本信息失败:${e.message}")
+            }
+
+        }.catch { e ->
+            LogUtils.e("网络接口初始化失败:${e.message}")
+        }
+
+    }
+
+}

+ 62 - 0
app/src/main/java/com/rdiot/yx485/adapter/CustomDataBindAdapter.kt

@@ -0,0 +1,62 @@
+package com.rdiot.yx485.adapter
+
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.databinding.BindingAdapter
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.resource.bitmap.CenterCrop
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.bumptech.glide.request.RequestOptions
+import com.rdiot.yx485.R
+import com.rdiot.yx485.base.LocalData
+import com.rdiot.yx485.util.circleImg
+import com.rdiot.yx485.util.dp
+
+/**
+ *
+ * @author mR2hao
+ * @date 2022/11/14
+ */
+object CustomDataBindAdapter {
+    @JvmStatic
+    @BindingAdapter(value = ["srcString", "radiusDp"], requireAll = false)
+    fun setImageResByString(img: ImageView, srcStr: String?, radius: Int) {
+        if (!srcStr.isNullOrBlank()) {
+            val id = img.context.resources.getIdentifier(srcStr, "mipmap", img.context.packageName)
+            if (id >= 0) {
+                if (radius == 0) {
+                    img.setImageResource(id)
+                } else {
+                    Glide.with(img.context)
+                        .load(id)
+                        .apply(
+                            RequestOptions().transform(CenterCrop(), RoundedCorners(radius.dp))
+                        )
+                        .into(img)
+                }
+
+            }
+        }
+    }
+
+
+    @JvmStatic
+    @BindingAdapter(value = ["circleImg"], requireAll = false)
+    fun setCircleImage(img: ImageView, srcStr: String) {
+        if (srcStr == "icon_member_add") {
+            setImageResByString(img, srcStr, 0)
+        } else {
+            img.circleImg(LocalData.apiHost.avatarHost + srcStr)
+        }
+    }
+
+    @JvmStatic
+    @BindingAdapter(value = ["mode", "lv"], requireAll = true)
+    fun setModeLevelText(tv: TextView, mode: Int, lv: Int) {
+        val context = tv.context
+        val modeStr = context.resources.getStringArray(R.array.modes)[mode]
+        val lvStr = context.resources.getStringArray(R.array.fan_speed)[lv]
+        tv.text = String.format(context.getString(R.string.mode_lv_str), modeStr, lvStr)
+    }
+
+}

+ 72 - 0
app/src/main/java/com/rdiot/yx485/adapter/FamilyAdapter.kt

@@ -0,0 +1,72 @@
+package com.rdiot.yx485.adapter
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.databinding.DataBindingUtil
+import androidx.recyclerview.widget.RecyclerView
+import com.rdiot.yx485.R
+import com.rdiot.yx485.bean.FamilyData
+import com.rdiot.yx485.bean.RoomData
+import com.rdiot.yx485.databinding.ItemFamilyBinding
+import com.rdiot.yx485.util.setClickLimitListener
+
+/**
+ * 家庭适配器
+ * @author mR2hao
+ * @date 2022/12/3
+ */
+class FamilyAdapter(
+    private val mListener: OnItemClickListener? = null
+) : RecyclerView.Adapter<FamilyAdapter.BaseViewHolder>() {
+    inner class BaseViewHolder(val dataBinding: ItemFamilyBinding) :
+        RecyclerView.ViewHolder(dataBinding.root)
+
+    private var mList: MutableList<FamilyData> = mutableListOf()
+
+    interface OnItemClickListener {
+        fun onItemClicked(position: Int, familyData: FamilyData)
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
+        val binding: ItemFamilyBinding = DataBindingUtil.inflate(
+            LayoutInflater.from(parent.context),
+            R.layout.item_family,
+            parent,
+            false
+        )
+
+        return BaseViewHolder(binding)
+    }
+
+    override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
+        val bean = mList[position]
+        holder.dataBinding.familyData = bean
+
+        holder.dataBinding.clItem.setClickLimitListener {
+            mListener?.onItemClicked(position, bean)
+        }
+    }
+
+    @Synchronized
+    fun update(list: List<FamilyData>) {
+        if (list.size < this.mList.size) {
+            mList.clear()
+            mList.addAll(list)
+            notifyDataSetChanged()
+        } else {
+            for (i in list.indices) {
+                if (i < this.mList.size) {
+                    if (this.mList[i] != list[i]) {
+                        this.mList[i] = list[i]
+                        notifyItemChanged(i)
+                    }
+                } else {
+                    this.mList.add(list[i])
+                    notifyItemInserted(i)
+                }
+            }
+        }
+    }
+
+    override fun getItemCount() = mList.size
+}

+ 157 - 0
app/src/main/java/com/rdiot/yx485/adapter/RoomsAdapter.kt

@@ -0,0 +1,157 @@
+package com.rdiot.yx485.adapter
+
+import android.graphics.Rect
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.databinding.DataBindingUtil
+import androidx.recyclerview.widget.RecyclerView
+import com.rdiot.yx485.R
+import com.rdiot.yx485.base.LocalData
+import com.rdiot.yx485.bean.RoomData
+import com.rdiot.yx485.databinding.ItemRoomBinding
+import com.rdiot.yx485.util.dp
+import com.rdiot.yx485.util.setClickLimitListener
+
+/**
+ * 房间列表
+ * @author mR2hao
+ * @date 2021/10/14
+ */
+class RoomsAdapter(
+    private val mListener: OnItemClickListener? = null
+) :
+    RecyclerView.Adapter<RoomsAdapter.RoomViewHolder>() {
+    /** 变量 是否为编辑模式 */
+    private var isEditMode = false
+    private var mList: MutableList<RoomData> = mutableListOf()
+
+    interface OnItemClickListener {
+        fun onItemClicked(position: Int, roomInfo: RoomData)
+        fun onSwitchClicked(position: Int, roomInfo: RoomData)
+        fun onDelClicked(position: Int, roomInfo: RoomData)
+    }
+
+    inner class RoomViewHolder(val binding: ItemRoomBinding) :
+        RecyclerView.ViewHolder(binding.root) {
+        fun bind(info: RoomData) {
+            binding.vm = info
+            binding.editMode = isEditMode
+            binding.executePendingBindings()
+        }
+    }
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RoomViewHolder {
+        val binding: ItemRoomBinding = DataBindingUtil.inflate(
+            LayoutInflater.from(parent.context),
+            R.layout.item_room,
+            parent,
+            false
+        )
+
+        return RoomViewHolder(binding)
+    }
+
+    override fun onBindViewHolder(holder: RoomViewHolder, position: Int) {
+        val bean = mList[position]
+        holder.bind(bean)
+
+        holder.binding.clItem.setClickLimitListener {
+            mListener?.onItemClicked(position, bean)
+        }
+
+        holder.binding.ivSwitch.setClickLimitListener {
+            mListener?.onSwitchClicked(position, bean)
+        }
+
+        holder.binding.ivDel.setClickLimitListener {
+            mListener?.onDelClicked(position, bean)
+        }
+
+    }
+
+    override fun getItemCount(): Int {
+        return mList.size
+    }
+
+    /** 切换是否为编辑模式 */
+    fun setEditMode(on: Boolean) {
+        if (isEditMode != on) {
+            isEditMode = on
+        }
+        notifyDataSetChanged()
+    }
+
+    fun isEditMode() = isEditMode
+
+    @Synchronized
+    fun update(list: List<RoomData>) {
+        if (list.size < this.mList.size) {
+            mList.clear()
+            mList.addAll(list)
+            notifyDataSetChanged()
+        } else {
+            for (i in list.indices) {
+                if (i < this.mList.size) {
+                    if (this.mList[i] != list[i]) {
+                        this.mList[i] = list[i]
+                        notifyItemChanged(i)
+                    }
+                } else {
+                    this.mList.add(list[i])
+                    notifyItemInserted(i)
+                }
+            }
+        }
+    }
+
+    fun removeRoom(roomData: RoomData) {
+        mList.remove(roomData)
+        val tempList = mutableListOf<RoomData>()
+        for (room in mList) {
+            tempList.add(room)
+        }
+        LocalData.familyData.postValue(
+            LocalData.familyData.value.apply {
+                this?.room = mList
+            }
+        )
+        notifyDataSetChanged()
+
+//        update(tempList)
+    }
+
+}
+
+class RoomItemDecoration(private val top: Int = 0, private val bottom: Int) :
+    RecyclerView.ItemDecoration() {
+    override fun getItemOffsets(
+        outRect: Rect,
+        view: View,
+        parent: RecyclerView,
+        state: RecyclerView.State
+    ) {
+        super.getItemOffsets(outRect, view, parent, state)
+
+        //获取当前要进行布局的item的position
+        when (parent.getChildLayoutPosition(view)) {
+            0, 1 -> {
+                outRect.top = top
+                outRect.bottom = bottom
+            }
+            else -> {
+                outRect.bottom = bottom
+            }
+        }
+
+        if (parent.getChildLayoutPosition(view) % 2 == 0) {
+            outRect.left = 15.dp
+            outRect.right = 5.dp
+        } else {
+            outRect.left = 5.dp
+            outRect.right = 15.dp
+        }
+
+    }
+
+}

+ 111 - 0
app/src/main/java/com/rdiot/yx485/adapter/ScenesAdapter.kt

@@ -0,0 +1,111 @@
+package com.rdiot.yx485.adapter
+
+import android.content.Context
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.recyclerview.widget.RecyclerView
+import com.ckr.pageview.adapter.BasePageAdapter
+import com.rdiot.yx485.R
+import com.rdiot.yx485.bean.SceneBean
+import com.rdiot.yx485.util.setClickLimitListener
+import kotlin.math.max
+
+/**
+ * 场景列表
+ */
+class ScenesAdapter(
+    context: Context?,
+    private val space: Int,
+    onItemClickListener: OnItemClickListener?
+) : BasePageAdapter<SceneBean?, ScenesAdapter.MainHolder?>(context) {
+
+    private var isShowDeleteIcon = false
+    private var mOnItemClickListener: OnItemClickListener? = null
+
+    override fun getItemCount(): Int {
+        val itemCount = super.getItemCount()
+        return if (mIsLooping) {
+            if (itemCount == 0) 0 else max(itemCount * 2, MIN_VALUE)
+        } else itemCount
+    }
+
+    override fun getLayoutId(viewType: Int): Int {
+        return R.layout.item_scene
+    }
+
+    override fun getViewHolder(itemView: View, viewType: Int): MainHolder {
+        return MainHolder(itemView)
+    }
+
+    override fun convert(
+        holder: MainHolder?,
+        position: Int,
+        originItem: SceneBean?,
+        adjustedPosition: Int,
+        item: SceneBean?
+    ) {
+        holder?.let {
+            if (item == null) {
+                holder.linearLayout.visibility = View.INVISIBLE
+                holder.itemView.setOnLongClickListener(null)
+                holder.itemView.setOnClickListener(null)
+            } else {
+
+                holder.linearLayout.visibility = View.VISIBLE
+                if (position % 3 == 0) {
+                    holder.linearLayout.setPadding(space/2, 0, space / 2, 0)
+                } else if (position % 3 == 1) {
+                    holder.linearLayout.setPadding(space / 2, 0, space / 2, 0)
+                } else if (position % 3 == 2) {
+                    holder.linearLayout.setPadding(space / 2, 0, space/2, 0)
+
+                }
+
+                holder.textView.text = item.name
+
+                holder.itemView.setClickLimitListener {
+                    mOnItemClickListener?.onClick(
+                        adjustedPosition,
+                        item
+                    )
+                }
+            }
+        }
+
+    }
+
+    class MainHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+        val textView: TextView
+        val constraintLayout: ConstraintLayout
+        val linearLayout: LinearLayout
+
+        init {
+            textView = itemView.findViewById<View>(R.id.tv_name) as TextView
+            constraintLayout = itemView.findViewById<View>(R.id.cl_item) as ConstraintLayout
+            linearLayout = itemView.findViewById<View>(R.id.ll_item) as LinearLayout
+        }
+    }
+
+    internal inner class OnItemLongClickListener(position: Int) : View.OnLongClickListener {
+        override fun onLongClick(v: View): Boolean {
+            isShowDeleteIcon = !isShowDeleteIcon
+            notifyDataSetChanged()
+            return true
+        }
+    }
+
+    interface OnItemClickListener {
+        fun onClick(position: Int, sceneBean: SceneBean?)
+    }
+
+    companion object {
+        const val MIN_VALUE = 6
+    }
+
+    init {
+        mOnItemClickListener = onItemClickListener
+    }
+
+}

+ 152 - 0
app/src/main/java/com/rdiot/yx485/base/AppManager.kt

@@ -0,0 +1,152 @@
+package com.rdiot.yx485.base
+
+import androidx.appcompat.app.AppCompatActivity
+import java.util.*
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlin.system.exitProcess
+
+/**
+ *
+ * @author mR2hao
+ * @date 2022/3/15
+ */
+@Singleton
+class AppManager @Inject constructor() {
+    init {
+        instance = this
+    }
+
+    /**
+     * 添加Activity到堆栈
+     */
+    fun addActivity(activity: AppCompatActivity?) {
+        activityStack.add(activity)
+    }
+
+    /**
+     * 获取栈顶Activity(堆栈中最后一个压入的)
+     */
+    fun getTopActivity(): AppCompatActivity {
+        return activityStack.lastElement()
+    }
+
+    /**
+     * 结束栈顶Activity(堆栈中最后一个压入的)
+     */
+    fun finishTopActivity() {
+        val activity: AppCompatActivity = activityStack.lastElement()
+        finishActivity(activity)
+    }
+
+    /**
+     * 结束指定类名的Activity
+     *
+     * @param cls
+     */
+    fun finishActivity(cls: Class<*>?) {
+        val iterator: MutableIterator<*> = activityStack.iterator()
+        while (iterator.hasNext()) {
+            val activity: AppCompatActivity = iterator.next() as AppCompatActivity
+            if (activity::class.java == cls) {
+                iterator.remove()
+                activity.finish()
+            }
+        }
+    }
+
+    /**
+     * 结束到指定类名的Activity
+     *
+     * @param cls
+     */
+    fun finishToActivity(cls: Class<*>?) {
+        var i = 0
+        val size: Int = activityStack.size
+        while (i < size) {
+            val lastActivity = activityStack.lastElement()
+            if (lastActivity::class.java != cls) {
+                activityStack.remove(lastActivity)
+                lastActivity.finish()
+            } else {
+                return
+            }
+            i++
+        }
+    }
+
+    /**
+     * 结束所有Activity
+     */
+    fun finishAllActivity() {
+        var i = 0
+        val size: Int = activityStack.size
+        while (i < size) {
+            if (null != activityStack[i]) {
+                activityStack[i].finish()
+            }
+            i++
+        }
+        activityStack.clear()
+    }
+
+    /**
+     * 退出应用程序
+     */
+    fun appExit() {
+        try {
+            finishAllActivity()
+            exitProcess(0)
+        } catch (e: Exception) {
+        }
+    }
+
+    /**
+     * 结束指定的Activity
+     */
+    fun finishActivity(activity: AppCompatActivity?) {
+        activity?.apply {
+            activityStack.remove(this)
+            finish()
+            overridePendingTransition(0, 0)
+        }
+    }
+
+    /**
+     * 得到指定类名的Activity
+     */
+    fun getActivity(cls: Class<*>?): AppCompatActivity? {
+        for (activity in activityStack) {
+            if (activity::class.java == cls) {
+                return activity
+            }
+        }
+        return null
+    }
+
+    fun recreateAllOtherActivity(activity: AppCompatActivity) {
+        var i = 0
+        val size: Int = activityStack.size
+        while (i < size) {
+            if (null != activityStack[i] && activityStack[i] !== activity) {
+                activityStack[i].recreate()
+            }
+            i++
+        }
+    }
+
+    companion object {
+        private var activityStack: Stack<AppCompatActivity> = Stack()
+
+        fun getActivityStack(): Stack<AppCompatActivity> {
+            return activityStack
+        }
+
+
+        /**
+         * 单一实例
+         */
+        lateinit var instance: AppManager
+
+    }
+}

+ 65 - 0
app/src/main/java/com/rdiot/yx485/base/BaseActivity.kt

@@ -0,0 +1,65 @@
+package com.rdiot.yx485.base
+
+import android.os.Bundle
+import android.view.MotionEvent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.databinding.DataBindingUtil
+import androidx.databinding.ViewDataBinding
+import com.rdiot.yx485.util.hideKeyboard
+import com.rdiot.yx485.util.isShouldHideKeyboard
+
+
+/**
+ *
+ * @author mR2hao
+ * @date 2021/8/2
+ */
+abstract class BaseActivity<TBinding : ViewDataBinding> : AppCompatActivity() {
+//    @Inject
+//    lateinit var appData: AppData
+
+    protected abstract fun getLayoutId(): Int
+
+    protected abstract fun initView(savedInstanceState: Bundle?)
+
+    protected lateinit var binding: TBinding
+
+    /** 是否前台显示 */
+    var isFront = false
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        AppManager().addActivity(this)
+        super.onCreate(savedInstanceState)
+        binding = DataBindingUtil.setContentView(this, getLayoutId())
+        initView(savedInstanceState)
+    }
+
+    override fun onDestroy() {
+        AppManager().finishActivity(this)
+        super.onDestroy()
+    }
+
+    /** 拦截点击事件,当输入法弹出时,点击非输入框位置就隐藏输入法 */
+    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
+        if (ev.action == MotionEvent.ACTION_DOWN) {
+            val v = currentFocus ?: return super.dispatchTouchEvent(ev)
+            if (v.isShouldHideKeyboard(ev)) {
+                val res = hideKeyboard(v.windowToken)
+                if (res) return true
+            }
+        }
+        return super.dispatchTouchEvent(ev)
+    }
+
+
+    override fun onResume() {
+        super.onResume()
+        isFront = true
+    }
+
+    override fun onStop() {
+        super.onStop()
+        isFront = false
+    }
+
+}

+ 59 - 0
app/src/main/java/com/rdiot/yx485/base/BaseFragment.kt

@@ -0,0 +1,59 @@
+package com.rdiot.yx485.base
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.databinding.DataBindingUtil
+import androidx.databinding.ViewDataBinding
+import androidx.fragment.app.Fragment
+
+/**
+ *
+ * @author mR2hao
+ * @date 2021/8/2
+ */
+abstract class BaseFragment<TBinding : ViewDataBinding> : Fragment() {
+
+//    @Inject
+//    lateinit var appData: AppData
+
+    lateinit var act: BaseActivity<TBinding>
+
+    protected abstract fun getLayoutId(): Int
+
+    /**
+     * 初始化界面元素和成员变量
+     * */
+    protected abstract fun initView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    )
+
+    private var _binding: TBinding? = null
+
+    val binding get() = _binding!!
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        act = requireActivity() as BaseActivity<TBinding>
+
+        _binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
+        binding.lifecycleOwner = viewLifecycleOwner
+
+        initView(inflater, container, savedInstanceState)//初始化界面元素和成员变量
+
+        return binding.root
+    }
+
+    override fun onDestroyView() {
+        super.onDestroyView()
+        _binding = null
+    }
+
+
+}

+ 46 - 0
app/src/main/java/com/rdiot/yx485/base/BaseMainPagerAdapter.kt

@@ -0,0 +1,46 @@
+package com.rdiot.yx485.base
+
+import android.view.ViewGroup
+import androidx.annotation.NonNull
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentManager
+import androidx.fragment.app.FragmentPagerAdapter
+
+/**
+ * 主页pagerAdapter
+ * @author mR2hao
+ * @date 2021/1/15
+ */
+class BaseMainPagerAdapter(private val fm: FragmentManager) :
+    FragmentPagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
+    private var mFragments = mutableListOf<Fragment>()
+
+    override fun getItem(position: Int): Fragment {
+        return mFragments[position]
+    }
+
+    override fun getCount(): Int {
+        return mFragments.size
+    }
+
+    fun setFragments(container: ViewGroup, @NonNull fragments: MutableList<Fragment>) {
+        for (i in fragments.indices) {
+            val fragment = findFragment(container.id, i) // 重点就是这里,会根据id去找是否有缓存的Fragment
+
+            if (fragment != null) { // 如果有就替换,不然用户看到的,和你实际使用的会是两个不同的Fragment
+                fragments[i] = fragment
+            }
+        }
+        mFragments = fragments
+    }
+
+    private fun findFragment(viewId: Int, position: Int): Fragment? {
+        val name = makeFragmentName(viewId, getItemId(position))
+        return fm.findFragmentByTag(name)
+    }
+
+    private fun makeFragmentName(viewId: Int, id: Long): String {
+        return "android:switcher:$viewId:$id"
+    }
+
+}

+ 54 - 0
app/src/main/java/com/rdiot/yx485/base/BaseViewModelFactory.kt

@@ -0,0 +1,54 @@
+package com.rdiot.yx485.base
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewmodel.CreationExtras
+import com.rdiot.yx485.ui.bind.model.BindViewModel
+import com.rdiot.yx485.ui.ctrl.model.RoomCtrlViewModel
+import com.rdiot.yx485.ui.family.model.FamilyDataViewModel
+import com.rdiot.yx485.ui.login.model.LoginViewModel
+
+/**
+ *
+ * @author mR2hao
+ * @date 2022/11/17
+ */
+class LoginViewModelFactory : ViewModelProvider.Factory {
+    override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
+        if (modelClass.isAssignableFrom(LoginViewModel::class.java)) {
+            @Suppress("UNCHECKED_CAST")
+            return LoginViewModel() as T
+        }
+        throw IllegalArgumentException("Unknown ViewModel class")
+    }
+}
+
+class BindModelFactory : ViewModelProvider.Factory {
+    override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
+        if (modelClass.isAssignableFrom(BindViewModel::class.java)) {
+            @Suppress("UNCHECKED_CAST")
+            return BindViewModel() as T
+        }
+        throw IllegalArgumentException("Unknown ViewModel class")
+    }
+}
+
+class FamilyDataModelFactory : ViewModelProvider.Factory {
+    override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
+        if (modelClass.isAssignableFrom(FamilyDataViewModel::class.java)) {
+            @Suppress("UNCHECKED_CAST")
+            return FamilyDataViewModel() as T
+        }
+        throw IllegalArgumentException("Unknown ViewModel class")
+    }
+}
+
+class RoomCtrlViewModelFactory : ViewModelProvider.Factory {
+    override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
+        if (modelClass.isAssignableFrom(RoomCtrlViewModel::class.java)) {
+            @Suppress("UNCHECKED_CAST")
+            return RoomCtrlViewModel() as T
+        }
+        throw IllegalArgumentException("Unknown ViewModel class")
+    }
+}

+ 89 - 0
app/src/main/java/com/rdiot/yx485/base/LocalData.kt

@@ -0,0 +1,89 @@
+package com.rdiot.yx485.base
+
+import androidx.lifecycle.MutableLiveData
+import com.drake.serialize.serialize.serialLazy
+import com.drake.serialize.serialize.serialLiveData
+import com.rdiot.yx485.BuildConfig
+import com.rdiot.yx485.bean.AppVersionData
+import com.rdiot.yx485.bean.FamilyData
+import com.rdiot.yx485.bean.LoginData
+import com.rdiot.yx485.bean.UserData
+import com.rdiot.yx485.bean.WeatherData
+import com.rdiot.yx485.bean.YXBean
+import com.rdiot.yx485.net.Api
+import com.rdiot.yx485.ui.login.model.LoginRecord
+
+/**
+ * 本地数据
+ * @author mR2hao
+ * @date 2022/11/17
+ */
+object LocalData {
+    /** Api地址 */
+    var apiHost: ApiHost by serialLazy(if (BuildConfig.DEBUG) ApiHost.TEST_HOST else ApiHost.PRODUCE_HOST)
+
+    /** 验证码倒计时剩余 */
+    var lastGetCodeTime: Long by serialLazy(0)
+
+    /** 登录数据 */
+    var loginData: LoginData? by serialLazy()
+
+    /** 用户数据 */
+    val userData by serialLiveData(UserData())
+
+    /** 用户家庭数据 selFamilyId为空时,获取默认家庭 */
+    val familyData by serialLiveData(FamilyData())
+
+    /** 天气数据 selFamilyId为空时,获取默认家庭*/
+    val weatherData by serialLiveData(WeatherData())
+
+    /** 所有家庭简单信息 */
+    val allFamily by serialLiveData(mutableListOf<FamilyData>())
+
+    /** 用户选择显示的家ID */
+    var selFamilyId: MutableLiveData<String?> = MutableLiveData(null)
+
+    /** 最后刷新时间 */
+    var lastRefreshTime: Long = 0
+
+    /** 最后一次登录类型 */
+    var lastLoginRecord: LoginRecord? by serialLazy()
+
+    /** 第一次安装app */
+    var firstTimeInstallApp: Boolean by serialLazy(true)
+
+    /** 移动端请求参数 */
+    val yxBean: YXBean = YXBean()
+
+    /** app版本信息 */
+    val appVersionData: MutableLiveData<AppVersionData?> = MutableLiveData(null)
+
+    /** 本日提醒更新APP用户(不判断年月,只判断日期,日期非本日则弹窗提醒) */
+    var noReminderToday: Int by serialLazy(0)
+
+    /** 测试 让Token失效 */
+    var makeTokenErr by serialLazy(false)
+
+    /** 登出 */
+    fun logout() {
+        lastGetCodeTime = 0
+        loginData = null
+        userData.value = null
+        familyData.value = null
+        weatherData.value = null
+        selFamilyId.value = null
+        allFamily.value = null
+    }
+}
+
+enum class ApiHost(
+    val hostName: String,
+    val host: String,
+    val avatarHost: String
+) {
+    /** 生产环境 */
+    PRODUCE_HOST("生产环境", Api.HOST, Api.AVATAR_HOST),
+
+    /** 测试环境 */
+    TEST_HOST("测试环境", Api.TEST_HOST, Api.TEST_AVATAR_HOST)
+}

+ 34 - 0
app/src/main/java/com/rdiot/yx485/bean/AppVersionData.kt

@@ -0,0 +1,34 @@
+package com.rdiot.yx485.bean
+
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/** APP版本信息 */
+@Serializable
+data class AppVersionData(
+    @SerialName("need_update")
+    var needUpdate: Boolean = false, // true
+    @SerialName("update_info")
+    var updateInfo: Info = Info()
+) {
+    @Serializable
+    data class Info(
+        @SerialName("content")
+        var content: String = "", // 测试更新
+        @SerialName("created_at")
+        var createdAt: String = "", // 2023-01-03T11:38:43+08:00
+        @SerialName("creator")
+        var creator: String = "",
+        @SerialName("download_url")
+        var downloadUrl: String = "", // http://www.baidu.com/
+        @SerialName("force_update")
+        var forceUpdate: Boolean = false, // true
+        @SerialName("platform")
+        var platform: String = "", // ANDROID
+        @SerialName("record_id")
+        var recordId: String = "", // 222
+        @SerialName("version")
+        var version: String = "" // 1.0.1
+    )
+}

+ 26 - 0
app/src/main/java/com/rdiot/yx485/bean/AvatarData.kt

@@ -0,0 +1,26 @@
+package com.rdiot.yx485.bean
+
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * 头像数据
+ * @property name String
+ * @property size Int
+ * @property url String
+ * @constructor
+ */
+@Serializable
+data class AvatarData(
+    @SerialName("name")
+    var name: String = "", // 1.png
+    @SerialName("size")
+    var size: Int = 0, // 562724
+    @SerialName("url")
+    var url: String = "" // /s/yongxu/1t7svi08nu0cp28ksqe7rnm500x1th1w/1.png
+) : java.io.Serializable {
+    companion object {
+        private const val serialVersionUID = -14074L
+    }
+}

+ 19 - 0
app/src/main/java/com/rdiot/yx485/bean/BaseResp.kt

@@ -0,0 +1,19 @@
+package com.rdiot.yx485.bean
+
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+@Serializable
+open class BaseResp<T>(
+    @SerialName("code")
+    var code: Int = 0, // 0
+    @SerialName("data")
+    var data: T? = null,
+    @SerialName("message")
+    var message: String = "" // success
+) {
+    fun isSucceed(): Boolean = code == 0
+}
+
+

+ 52 - 0
app/src/main/java/com/rdiot/yx485/bean/FamilyData.kt

@@ -0,0 +1,52 @@
+package com.rdiot.yx485.bean
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+
+/**
+ * 家庭信息
+ * @author mR2hao
+ * @date 2022/12/2
+ */
+@Serializable
+data class FamilyData(
+    @SerialName("ad_code")
+    var adCode: String = "",
+    @SerialName("address")
+    var address: String = "",
+    @SerialName("admin_id")
+    var adminId: String = "", // 1t7svi0l6m4codj7wi6ctg1400fu5m0w
+    @SerialName("admin_name")
+    var adminName: String = "",
+    @SerialName("city")
+    var city: String = "",
+    @SerialName("is_default")
+    var isDefault: Boolean = false,
+    @SerialName("district")
+    var district: String = "",
+    @SerialName("gateway")
+    var gateway: String = "",
+    @SerialName("is_online")
+    var isOnline: Boolean = false,
+    @SerialName("member")
+    var member: List<MemberData> = listOf(),
+    @SerialName("name")
+    var name: String = "", // 我的家
+    @SerialName("power")
+    var power: Boolean = false,
+    @SerialName("province")
+    var province: String = "",
+    @SerialName("qr_code")
+    var qrCode: String = "", //
+    @SerialName("record_id")
+    var recordId: String = "", // 1t7svi0l6m4codj7wi7dd1u500cqkuzk
+    @SerialName("room")
+    var room: List<RoomData>? = null
+) : java.io.Serializable {
+    companion object {
+        private const val serialVersionUID = -54L
+    }
+
+}
+

+ 55 - 0
app/src/main/java/com/rdiot/yx485/bean/LoginData.kt

@@ -0,0 +1,55 @@
+package com.rdiot.yx485.bean
+
+
+import com.rdiot.yx485.base.LocalData
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * 登录数据
+ * @author mR2hao
+ * @date 2022/11/19
+ */
+@Serializable
+data class LoginData(
+    /**
+     * 访问令牌
+     */
+    @SerialName("access_token")
+    var accessToken: String = "",
+    /**
+     * app是否首次登录
+     */
+    @SerialName("app_first_login")
+    var appFirstLogin: Boolean = false,
+
+    /**
+     * 令牌到期时间
+     */
+    @SerialName("expires_at")
+    var expiresAt: Int = 0,
+
+    /**
+     * 令牌类型
+     */
+    @SerialName("token_type")
+    var tokenType: String = ""
+) : java.io.Serializable {
+
+    companion object {
+        /** 清除本地 */
+        fun clearLocal() {
+            LocalData.loginData = null
+        }
+
+        private const val serialVersionUID = -98L
+    }
+
+    /** 本地保存 */
+    fun saveLocal(): LoginData {
+        LocalData.loginData = this
+        return this
+    }
+
+}
+

+ 38 - 0
app/src/main/java/com/rdiot/yx485/bean/MemberData.kt

@@ -0,0 +1,38 @@
+package com.rdiot.yx485.bean
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ *
+ * @author mR2hao
+ * @date 2022/12/2
+ */
+@Serializable
+data class MemberData(
+    @SerialName("creator")
+    var creator: String = "",
+    @SerialName("home_id")
+    var homeId: String = "", // 1t7svi0l6m4codj7wi7dd1u500cqkuzk
+    @SerialName("is_default")
+    var isDefault:Boolean = false, // 1
+    @SerialName("phone")
+    var phone: String = "", // 18689308183
+    @SerialName("photo")
+    var photo: String = "",
+    @SerialName("real_name")
+    var realName: String = "",
+    @SerialName("record_id")
+    var recordId: String = "", // 1t7svi0l6m4codj7wi6ctg1400fu5m0w
+    @SerialName("user_id")
+    var userId: String = "", // 1t7svi0l6m4codj7wi6ctg1400fu5m0w
+    @SerialName("user_name")
+    var userName: String = "", // mR.2hao
+
+    /** 编辑模式 */
+    var isEditMode: Boolean = false
+) : java.io.Serializable {
+    companion object {
+        private const  val serialVersionUID = -46L
+    }
+}

+ 82 - 0
app/src/main/java/com/rdiot/yx485/bean/RoomData.kt

@@ -0,0 +1,82 @@
+package com.rdiot.yx485.bean
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * 房间信息
+ */
+@Serializable
+data class RoomData(
+    /** 分控编号 */
+    @SerialName("control_number")
+    var controlNumber: String = "",
+    /** 定时时长 */
+    @SerialName("duration")
+    var duration: Float = 0f, // 0
+    /** 风速(0:自动; 1:1档; 2:2 档; 3: 3 档; 4:4 档; 5: 5 档; 6:超强档;) */
+    @SerialName("fan_speed")
+    var fanSpeed: Int = 1, // 0
+    /** 风阀档位 */
+    @SerialName("fan_value")
+    var fanValue: Int = 0, // 0
+    /** 家id */
+    @SerialName("home_id")
+    var homeId: String = "", // 1t7svi0l6m4codj7wi7dd1u500cqkuzk
+    /** 家名称 */
+    @SerialName("home_name")
+    var homeName: String = "",
+    /** 湿度 */
+    @SerialName("humidity")
+    var humidity: Int = 0, // 0
+    /** 是否是主控(1是2否) */
+    @SerialName("is_master")
+    var isMaster: Boolean = false, // 1
+    /** 模式(1:制冷,2:制热,3:除湿4:送风5:加湿) */
+    @SerialName("mode")
+    var mode: Int = 1, // 1
+    /** 新风系统开关 */
+    @SerialName("new_fan")
+    var freshAir: Boolean = false,
+    /** 名称 */
+    @SerialName("name")
+    var name: String = "", // 随意房间
+    /** 主控状态 (0关 1开) */
+    @SerialName("power")
+    var power: Boolean = false, // 0
+    /** 记录id */
+    @SerialName("record_id")
+    var recordId: String = "", // 1t7svi039tdcoqd4p4g2ncy100mvdglz
+    /** 当前设置的温度 */
+    @SerialName("set_temp")
+    var setTemp: Int = 25, // 0
+    /** 温度 */
+    @SerialName("temperature")
+    var temperature: Int = 0, // 0
+    /** 空气质量 PM2.5 */
+    @SerialName("air_quality")
+    var pm25: Int = 0,
+    /** 二氧化碳 */
+    @SerialName("co2")
+    var co2: Int = 0,
+    /** 定时状态(1:有定时;0:无定时) */
+    @SerialName("timer_status")
+    var timerStatus: Boolean = false, // 0
+    /** 类型 */
+    @SerialName("type")
+    var type: String = "",
+    /** 用户id */
+    @SerialName("user_id")
+    var userId: String = "",
+
+
+    /** 编辑模式 */
+    var isEditMode: Boolean = false
+) : java.io.Serializable {
+
+    companion object {
+        private const val serialVersionUID = -85L
+    }
+
+
+}

+ 12 - 0
app/src/main/java/com/rdiot/yx485/bean/SceneBean.kt

@@ -0,0 +1,12 @@
+package com.rdiot.yx485.bean
+
+/**
+ *
+ * @author mR2hao
+ * @date 2022/11/10
+ */
+data class SceneBean(
+    /** 场景名 */
+    var name: String = ""
+){
+}

+ 71 - 0
app/src/main/java/com/rdiot/yx485/bean/UserData.kt

@@ -0,0 +1,71 @@
+package com.rdiot.yx485.bean
+
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+/**
+ * 用户信息
+ * @author mR2hao
+ * @date 2022/11/21
+ */
+@Serializable
+data class UserData(
+    /** 创建时间 */
+    @SerialName("created_at")
+    var createdAt: String = "", // 2022-11-16T15:12:28.499+08:00
+
+    @SerialName("creator")
+    var creator: String = "",
+
+    /** 默认家id */
+    @SerialName("default_home_id")
+    var defaultHomeId: String = "", // 1t7svi0l6m4codj7wi7dd1u500cqkuzk
+
+    /** 默认家名称 */
+    @SerialName("default_home_name")
+    var defaultHomeName: String = "", // 我的家
+
+    /** 密码? */
+    @SerialName("password")
+    var password: String = "", // ******
+
+    /** 手机号 */
+    @SerialName("phone")
+    var phone: String = "", // 18689308183
+
+    /** 头像 */
+    @SerialName("photo")
+    var photo: String = "",
+
+    /** 真实姓名 */
+    @SerialName("real_name")
+    var realName: String = "",
+
+    /** 记录id */
+    @SerialName("record_id")
+    var recordId: String = "", // 1t7svi0l6m4codj7wi6ctg1400fu5m0w
+
+    /** 用户状态(1:启动 2:停用) */
+    @SerialName("status")
+    var status: Int = 0, // 1
+
+    /** 更新时间 */
+    @SerialName("updated_at")
+    var updatedAt: String = "", // 2022-11-16T15:12:28.499+08:00
+
+    /** 用户名 */
+    @SerialName("user_name")
+    var userName: String = "", // 18689308183
+
+    /** 是否已经设置密码 */
+    @SerialName("set_password")
+    var setPwd:Boolean =false
+) : java.io.Serializable {
+
+    companion object {
+
+        private const  val serialVersionUID = -4798L
+    }
+
+}

+ 55 - 0
app/src/main/java/com/rdiot/yx485/bean/WeatherData.kt

@@ -0,0 +1,55 @@
+package com.rdiot.yx485.bean
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+
+
+@Serializable
+data class WeatherData(
+    @SerialName("location")
+    val location: Location? = null,
+    @SerialName("now")
+    val now: Now? = null
+) {
+    @Serializable
+    data class Location(
+        @SerialName("city")
+        val city: String,
+        @SerialName("country")
+        val country: String,
+        @SerialName("id")
+        val id: String,
+        @SerialName("name")
+        val name: String,
+        @SerialName("province")
+        val province: String
+    )
+
+    @Serializable
+    data class Now(
+        /** 天气图标代码 */
+        @SerialName("icon_code")
+        val iconCode: String,
+        /** 体感温度 */
+        @SerialName("feels_like")
+        val feelsLike: Int,
+        /** 相对湿度 */
+        @SerialName("rh")
+        val rh: Int,
+        /** 温度 */
+        @SerialName("temp")
+        val temp: Int,
+        /** 天气现象 */
+        @SerialName("text")
+        val text: String = "未知",
+        /** 更新时间 */
+        @SerialName("uptime")
+        val uptime: String,
+        /** 风力等级 */
+        @SerialName("wind_class")
+        val windClass: String,
+        /** 风向 */
+        @SerialName("wind_dir")
+        val windDir: String
+    )
+}

+ 106 - 0
app/src/main/java/com/rdiot/yx485/bean/YXBean.kt

@@ -0,0 +1,106 @@
+package com.rdiot.yx485.bean
+
+
+import android.os.Build
+import android.util.Base64
+import com.rdiot.yx485.BuildConfig
+import com.rdiot.yx485.util.curTime
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
+
+@Serializable
+data class YXBean(
+    /** 渠道 */
+    @SerialName("channel")
+    var channel: String = "",
+    /** 设备ID */
+    @SerialName("device_id")
+    var deviceId: String = "",
+    /** 设备类型 */
+    @SerialName("device_type")
+    var deviceType: String = "",
+    /** 定位(经度,纬度) */
+    @SerialName("location")
+    var location: String = "",
+    /** mqtt令牌 */
+    @SerialName("mqtt_token")
+    var mqttToken: String = "",
+    /** 网络类型 */
+    @SerialName("net_type")
+    var netType: String = "",
+    /** 操作系统 */
+    @SerialName("os")
+    var os: String = "",
+    /** 系统版本 */
+    @SerialName("os_version")
+    var osVersion: String = "",
+    /** 手机厂商 */
+    @SerialName("phone_brand")
+    var phoneBrand: String = "",
+    /** 手机型号 */
+    @SerialName("phone_model")
+    var phoneModel: String = "",
+    /** 分辨率 高 */
+    @SerialName("screen_height")
+    var screenHeight: Int = 0,
+    /** 分辨率 宽 */
+    @SerialName("screen_width")
+    var screenWidth: Int = 0,
+    /** 来源渠道 */
+    @SerialName("source")
+    var source: String = "",
+    /** 时间戳 */
+    @SerialName("timestamp")
+    var timestamp: Long = 0,
+    /** 用户ID */
+    @SerialName("user_id")
+    var userId: String = "",
+    /** 用户类型 */
+    @SerialName("user_type")
+    var userType: String = "",
+    /** UUID */
+    @SerialName("uuid")
+    var uuid: String = "",
+    /** 应用版本号 */
+    @SerialName("version")
+    var version: String = ""
+) : java.io.Serializable {
+
+    init {
+        channel = "Android"
+        deviceId = ""
+        deviceType = "Android"
+        location = ""
+        mqttToken = ""
+        netType = ""
+        os = ""
+        osVersion = Build.VERSION.RELEASE
+        phoneBrand = Build.BRAND
+        phoneModel = Build.MODEL
+        screenHeight = 0
+        screenWidth = 0
+        source = ""
+        userId = ""
+        userType = ""
+        uuid = ""
+        version = BuildConfig.VERSION_NAME
+    }
+
+    fun toJsonString(): String {
+        timestamp = curTime
+        return Json.encodeToString(this)
+    }
+
+    fun toBase64(): String {
+        return Base64.encodeToString(
+            toJsonString().toByteArray(),
+            Base64.DEFAULT
+        ).trim().replace("\n", "")
+    }
+
+    companion object {
+        private const val serialVersionUID = -48L
+    }
+}

+ 445 - 0
app/src/main/java/com/rdiot/yx485/net/Api.kt

@@ -0,0 +1,445 @@
+package com.rdiot.yx485.net
+
+import android.net.Uri
+import com.drake.net.*
+import com.drake.net.interfaces.ProgressListener
+import com.rdiot.yx485.BuildConfig
+import com.rdiot.yx485.base.LocalData
+import com.rdiot.yx485.bean.*
+import com.rdiot.yx485.util.md5
+import kotlinx.coroutines.CoroutineScope
+import java.io.File
+
+/**
+ *
+ * @author mR2hao
+ * @date 2022/11/16
+ */
+object Api {
+    /** Api地址 */
+    const val TEST_HOST = "http://yongxu.yehaoji.cn:8199/api/v1/"
+
+    /** 头像地址 */
+    const val TEST_AVATAR_HOST = "http://yongxu.yehaoji.cn:18199"
+
+    /** Api地址 */
+    const val HOST = "https://app.yongxulvjian.com/api/v1/"
+
+    /** 头像地址 */
+    const val AVATAR_HOST = "https://app.yongxulvjian.com"
+
+    /** App接口 */
+    object App {
+        /** 初始化 */
+        const val BASE = "init"
+
+        /** 检测版本 */
+        const val CHECK_VERSION = "check_version"
+
+        /** 天气 */
+        const val WEATHER = "homepages/weather"
+    }
+
+    /** 登录接口 */
+    object Login {
+        private const val BASE = "login"
+
+        /** 获取验证码 */
+        const val GET_SMS_CODE = "${BASE}/code"
+
+        /** 验证码登录 */
+        const val SMS_LOGIN = "${BASE}/sms"
+
+        /** 验证码登录 */
+        const val PWD_LOGIN = "${BASE}/password"
+
+        /** 登出 */
+        const val SIGN_OUT = "${BASE}/exit"
+
+        /** 注销 */
+        const val LOG_OUT = "${BASE}/unsubscribe"
+
+        /** 设置密码 */
+        const val SET_PWD = "${BASE}/set_password"
+
+        /** 更新密码 */
+        const val RESET_PWD = "${BASE}/update_pass"
+
+        /** 刷新Token */
+        const val REFRESH_TOKEN = "${BASE}/refresh/token"
+    }
+
+    /** 用户接口 */
+    object User {
+        private const val BASE = "users"
+
+        /** 获取当前用户信息 */
+        const val GET_CURRENT_USER_DATA = "${BASE}/current"
+
+        /** 用户信息 */
+        const val USER_INFO = "${BASE}/%s"
+
+    }
+
+    /** 家庭管理接口 */
+    object Family {
+        const val BASE = "homes"
+
+        /** 家庭成员管理 */
+        const val MEMBER = "family_members"
+
+        /** 默认家 */
+        const val DEFAULT_FAMILY = "${BASE}/default"
+
+        /** 家列表 */
+        const val ALL_FAMILY = "${BASE}?q=list"
+
+        /** 获取指定ID的家庭信息 */
+        const val FAMILY_INFO = "${BASE}/%s"
+
+        /** 添加家庭成员 */
+        const val INVITE_MEMBER = "${MEMBER}/invite"
+
+        /** 设置默认家庭 */
+        const val SET_DEFAULT_FAMILY = "${MEMBER}/default/%s"
+    }
+
+    /** 房间管理 */
+    object Room {
+        const val BASE = "rooms"
+
+        /** 获取指定ID的房间信息 */
+        const val GET_ROOM_INFO = "${BASE}/%s"
+    }
+
+    /** 设备控制 */
+    object Ctrl {
+        private const val BASE = "control"
+
+        /** 总开关 */
+        const val POWER = "${BASE}/power"
+
+        /** 新风开关 */
+        const val FRESH_AIR = "${BASE}/new_fan"
+
+        /** 模式 */
+        const val MODE = "${BASE}/mode"
+
+        /** 温度 */
+        const val TEMP = "${BASE}/temp"
+
+        /** 定时 */
+        const val TIMER = "${BASE}/timer"
+
+        /** 主控风速 */
+        const val WIND_SPEED = "${BASE}/speed"
+
+        /** 分控开关 */
+        const val SUB_POWER = "${BASE}/fan_power"
+
+        /** 分控风速 */
+        const val SUB_WIND_SPEED = "${BASE}/fan_value"
+    }
+
+    object File {
+        const val BASE = "files"
+
+    }
+}
+
+/************************** App相关 **************************/
+fun CoroutineScope.refreshToken() = Put<LoginData>(Api.Login.REFRESH_TOKEN)
+
+/** 初始化 */
+fun CoroutineScope.appInit() = Post<BaseResp<String>>(Api.App.BASE)
+
+/** 检查版本号 */
+fun CoroutineScope.checkVersion() = Post<AppVersionData>(Api.App.CHECK_VERSION) {
+    json(
+        "version" to BuildConfig.VERSION_NAME,
+        "platform" to "ANDROID"
+    )
+}
+
+/** 获取天气 */
+fun CoroutineScope.getWeather(familyId: String? = null) = Get<WeatherData>(Api.App.WEATHER) {
+    param("home_id", familyId ?: LocalData.selFamilyId.value)
+}
+
+/************************** 登录、用户相关 **************************/
+/** 短信类型 */
+enum class SmsLimit(val limitType: Int) {
+    /** 登录 */
+    LOGIN(1),
+
+    /** 忘记密码 */
+    RESET_PWD(2)
+}
+
+/** 获取手机短信 */
+fun CoroutineScope.getSms(phone: String?, limit: SmsLimit) =
+    Post<BaseResp<String>>(Api.Login.GET_SMS_CODE) {
+        json(
+            "tel" to phone,
+            "bzty" to limit.limitType,
+            "resend_limit" to 1,
+            "expired" to 999999999,
+        )
+    }
+
+/** 短信登录 */
+fun CoroutineScope.loginViaSMS(phone: String?, code: String?) =
+    Post<LoginData>(Api.Login.SMS_LOGIN) {
+        json(
+            "phone" to phone,
+            "code" to code
+        )
+    }
+
+/** 密码登录 */
+fun CoroutineScope.loginViaPwd(phone: String?, password: String?) =
+    Post<LoginData>(Api.Login.PWD_LOGIN) {
+        json(
+            "phone" to phone,
+            "password" to password?.md5()
+        )
+    }
+
+/** (首次登录)设置密码 */
+fun CoroutineScope.setPwd(pwd: String) = Put<BaseResp<String>>(Api.Login.SET_PWD) {
+    json(
+        "password" to pwd.md5()
+    )
+}
+
+/** 更新密码(找回、重置密码) */
+fun CoroutineScope.resetPwd(phone: String?, code: String?, newPwd: String?) =
+    Put<BaseResp<String>>(Api.Login.RESET_PWD) {
+        json(
+            "phone" to phone,
+            "code" to code,
+            "new_password" to newPwd?.md5()
+        )
+    }
+
+/** 注销账户 */
+fun CoroutineScope.logOut() = Patch<BaseResp<String>>(Api.Login.LOG_OUT)
+
+/** 登出 */
+fun CoroutineScope.signOut() = Post<BaseResp<String>>(Api.Login.SIGN_OUT)
+
+/** 获取当前用户信息 */
+fun CoroutineScope.getCurrentUserData() = Get<UserData>(Api.User.GET_CURRENT_USER_DATA)
+
+/** 头像上传 */
+fun CoroutineScope.uploadAvatar(imgUri: Uri, listener: ProgressListener?) =
+    Post<AvatarData>(Api.File.BASE) {
+        param("file", File(imgUri.path))
+        listener?.let {
+            addUploadListener(it)
+        }
+    }
+
+/** 通过手机号获取用户信息 */
+fun CoroutineScope.getUserData(phone: String) =
+    Get<UserData>(String.format(Api.User.USER_INFO, phone))
+
+/** 更新用户信息 */
+fun CoroutineScope.updateUserData(userData: UserData) =
+    Put<BaseResp<String>>(String.format(Api.User.USER_INFO, userData.recordId)) {
+        json(
+            "user_name" to userData.userName,
+            "photo" to userData.photo
+        )
+    }
+
+
+/************************** 家庭管理 **************************/
+/** 获取指定ID家信息,传入ID为空时候,默认为选择的家庭ID */
+fun CoroutineScope.getFamilyData(familyId: String? = null) =
+    Get<FamilyData>(
+        String.format(
+            Api.Family.FAMILY_INFO,
+            familyId ?: LocalData.selFamilyId.value
+        )
+    )
+
+/** 获取家列表 */
+fun CoroutineScope.getAllFamilyData(isOnlyShowAdmin: Boolean) =
+    Get<List<FamilyData>>(if (isOnlyShowAdmin) "${Api.Family.ALL_FAMILY}&is_admin=1" else Api.Family.ALL_FAMILY)
+
+/** 更新信息家庭 */
+fun CoroutineScope.updateFamilyData(familyData: FamilyData) = Put<BaseResp<String>>(
+    String.format(
+        Api.Family.FAMILY_INFO,
+        familyData.recordId
+    )
+) {
+    json(
+        "name" to familyData.name,
+        "city" to familyData.city,
+    )
+}
+
+/** 邀请用户 */
+fun CoroutineScope.inviteMember(familyData: FamilyData?, phone: String) =
+    Post<BaseResp<String>>(Api.Family.INVITE_MEMBER) {
+        json(
+            "home_id" to familyData?.recordId,
+            "phone" to phone
+        )
+    }
+
+/** 删除用户 */
+fun CoroutineScope.deleteMember(memberData: MemberData) =
+    Delete<BaseResp<String>>(Api.Family.MEMBER) {
+        json(
+            "home_id" to memberData.homeId,
+            "user_id" to memberData.userId
+        )
+    }
+
+/** 通过家庭Id 设置默认家庭 */
+fun CoroutineScope.setDefaultFamily(familyId: String?) =
+    Put<BaseResp<String>>(String.format(Api.Family.SET_DEFAULT_FAMILY, familyId))
+
+/** 新建家庭 */
+fun CoroutineScope.addFamily(familyName: String, isSetDefault: Boolean) =
+    Post<BaseResp<String>>(Api.Family.BASE) {
+        json(
+            "name" to familyName,
+            "is_default" to isSetDefault
+        )
+    }
+
+/** 删除家庭 */
+fun CoroutineScope.deleteFamily(familyId: String?) =
+    Delete<BaseResp<String>>(String.format(Api.Family.FAMILY_INFO, familyId))
+
+/************************** 房间管理 **************************/
+/** 添加房间(分控) */
+fun CoroutineScope.addRoom(roomName: String, ctrlNum: String) =
+    Post<BaseResp<String>>(Api.Room.BASE) {
+        json(
+            "name" to roomName,
+            "home_id" to LocalData.selFamilyId.value,
+            "control_number" to ctrlNum
+        )
+    }
+
+/** 删除房间 */
+fun CoroutineScope.delRoom(roomArray: List<RoomData>) = Delete<BaseResp<String>>(Api.Room.BASE) {
+    val idArray = arrayListOf<String>()
+    for (roomData in roomArray) {
+        idArray.add(roomData.recordId)
+    }
+    json(
+        "home_id" to LocalData.selFamilyId.value,
+        "record_ids" to idArray
+    )
+}
+
+/** 更新房间信息 */
+fun CoroutineScope.updateRoomInfo(roomData: RoomData) =
+    Put<BaseResp<String>>(String.format(Api.Room.GET_ROOM_INFO, roomData.recordId)) {
+        json(
+            "name" to roomData.name,
+            "home_id" to roomData.homeId,
+            "control_number" to roomData.controlNumber
+        )
+    }
+
+/** 获取指定Id房间信息 */
+fun CoroutineScope.getRoomInfo(roomId: String) =
+    Get<RoomData>(String.format(Api.Room.GET_ROOM_INFO, roomId))
+
+
+/************************** 设备控制 **************************/
+/** 总控开关 */
+fun CoroutineScope.setPower(gateway: String, roomData: RoomData) =
+    Post<BaseResp<String>>(if (roomData.isMaster) Api.Ctrl.POWER else Api.Ctrl.SUB_POWER) {
+        json(
+            "gateway" to gateway,
+            "control_number" to roomData.controlNumber,
+            "power" to roomData.power
+        )
+    }
+
+enum class ModeType(val typeCode: Int) {
+    /** 制冷 */
+    ZHI_LENG(0),
+
+    /** 制热 */
+    ZHI_RE(1),
+
+    /** 除湿 */
+    CHU_SHI(2),
+
+    /** 送风 */
+    SONG_FENG(3),
+
+    /** 加湿 */
+    JIA_SHI(4)
+
+}
+
+/** 设置模式 */
+fun CoroutineScope.setMode(gateway: String, roomData: RoomData, modeType: ModeType) =
+    Post<BaseResp<String>>(Api.Ctrl.MODE) {
+        json(
+            "gateway" to gateway,
+            "control_number" to roomData.controlNumber,
+            "mode" to modeType.typeCode
+        )
+    }
+
+fun CoroutineScope.setFreshAir(gateway: String, roomData: RoomData) =
+    Post<BaseResp<String>>(Api.Ctrl.FRESH_AIR) {
+        json(
+            "gateway" to gateway,
+            "control_number" to roomData.controlNumber,
+            "power" to roomData.freshAir
+        )
+    }
+
+/** 设置定时 */
+fun CoroutineScope.setTimer(
+    gateway: String,
+    roomData: RoomData,
+    enable: Boolean,
+    time: Float? = null
+) = Post<BaseResp<String>>(Api.Ctrl.TIMER) {
+    json(
+        "gateway" to gateway,
+        "control_number" to roomData.controlNumber,
+        "timer_status" to enable,
+        "timer" to time
+    )
+}
+
+/** 设置温度 */
+fun CoroutineScope.setTemp(gateway: String, roomData: RoomData, temp: Int) =
+    Post<BaseResp<String>>(Api.Ctrl.TEMP) {
+        json(
+            "gateway" to gateway,
+            "control_number" to roomData.controlNumber,
+            "temp" to temp
+        )
+    }
+
+/** 设置风速 */
+fun CoroutineScope.setWindSpeed(
+    gateway: String,
+    roomData: RoomData,
+    speed: Int,
+    isMaster: Boolean
+) =
+    Post<BaseResp<String>>(if (isMaster) Api.Ctrl.WIND_SPEED else Api.Ctrl.SUB_WIND_SPEED) {
+        json(
+            "gateway" to gateway,
+            "control_number" to roomData.controlNumber,
+            "fan_value" to speed,
+            "speed" to speed
+        )
+    }
+

+ 32 - 0
app/src/main/java/com/rdiot/yx485/net/MyRequestInterceptor.kt

@@ -0,0 +1,32 @@
+package com.rdiot.yx485.net
+
+import com.drake.net.interceptor.RequestInterceptor
+import com.drake.net.request.BaseRequest
+import com.rdiot.yx485.base.LocalData
+
+
+/** 请求拦截器, 一般用于添加全局参数 */
+
+
+class MyRequestInterceptor : RequestInterceptor {
+    /** 本方法每次请求发起都会调用, 这里添加的参数可以是动态参数 */
+    override fun interceptor(request: BaseRequest) {
+        // 仅请求动作没有添加时才会添加默认请求头
+        if (request.headers()["X-YX-APP"] == null) {
+            val jsonStr = LocalData.yxBean.toBase64()
+            request.addHeader("X-YX-APP", jsonStr)
+        }
+
+        LocalData.loginData?.let { loginData ->
+            val token = "${loginData.tokenType} ${loginData.accessToken}"
+//            LogUtils.e("Token:$token")
+            if (LocalData.makeTokenErr) {
+                request.addHeader("Authorization", "${token}1")
+                LocalData.makeTokenErr = false
+            } else {
+                request.addHeader("Authorization", token)
+            }
+        }
+
+    }
+}

+ 80 - 0
app/src/main/java/com/rdiot/yx485/net/SerializationConverter.kt

@@ -0,0 +1,80 @@
+@file:Suppress("UNCHECKED_CAST", "MemberVisibilityCanBePrivate")
+
+package com.rdiot.yx485.net
+
+import com.drake.net.NetConfig
+import com.drake.net.convert.NetConverter
+import com.drake.net.exception.ConvertException
+import com.drake.net.exception.RequestParamsException
+import com.drake.net.exception.ResponseException
+import com.drake.net.exception.ServerResponseException
+import com.drake.net.request.kType
+import kotlinx.serialization.SerializationException
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.serializer
+import okhttp3.Response
+import org.json.JSONObject
+import java.lang.reflect.Type
+import kotlin.reflect.KType
+
+
+
+class SerializationConverter(
+    val success: String = "0",
+    val code: String = "code",
+    val message: String = "message",
+) : NetConverter {
+
+    companion object {
+        val jsonDecoder = Json {
+            ignoreUnknownKeys = true // JSON和数据模型字段可以不匹配
+            coerceInputValues = true // 如果JSON字段是Null则使用默认值
+            allowStructuredMapKeys = true //允许结构化映射(map的key可以使用对象)
+            prettyPrint = true
+        }
+
+        const val tempJson = "{\"data\":\"\"}"
+    }
+
+    override fun <R> onConvert(succeed: Type, response: Response): R? {
+        try {
+            return NetConverter.onConvert<R>(succeed, response)
+        } catch (e: ConvertException) {
+            val code = response.code
+            when {
+                code in 200..299 -> { // 请求成功
+                    val bodyString = response.body?.string() ?: return null
+                    val kType = response.request.kType
+                        ?: throw ConvertException(response, "Request does not contain KType")
+                    return try {
+                        val json = JSONObject(bodyString) // 获取JSON中后端定义的错误码和错误信息
+                        val srvCode = json.getString(this.code)
+                        if (srvCode == success) { // 对比后端自定义错误码
+                            val dataStr= json.getString("data")
+//                            LogUtils.e(dataStr)
+                            dataStr.parseBody<R>(kType)
+
+//                            if (!dataStr.isNullOrBlank()){
+//                                json.getString("data").parseBody<R>(kType)
+//                            }else{
+//                                null
+//                            }
+                        } else { // 错误码匹配失败, 开始写入错误异常
+                            val errorMessage = json.optString(message, NetConfig.app.getString(com.drake.net.R.string.no_error_message))
+                            throw ResponseException(response, errorMessage, tag = srvCode) // 将业务错误码作为tag传递
+                        }
+                    } catch (e: SerializationException) { // 固定格式JSON分析失败直接解析JSON
+                        bodyString.parseBody<R>(kType)
+                    }
+                }
+                code in 400..499 -> throw RequestParamsException(response, response.message) // 请求参数错误
+                code >= 500 -> throw ServerResponseException(response, response.message) // 服务器异常错误
+                else -> throw ConvertException(response)
+            }
+        }
+    }
+
+    fun <R> String.parseBody(succeed: KType): R? {
+        return jsonDecoder.decodeFromString(Json.serializersModule.serializer(succeed), this) as R
+    }
+}

+ 7 - 0
app/src/main/java/com/rdiot/yx485/net/update/DownloadListener.kt

@@ -0,0 +1,7 @@
+package com.rdiot.yx485.net.update
+
+interface DownloadListener {
+    fun onStart()
+    fun onProgress(progress: Float)
+    fun onFinish()
+}

+ 95 - 0
app/src/main/java/com/rdiot/yx485/net/update/DownloadTask.kt

@@ -0,0 +1,95 @@
+package com.rdiot.yx485.net.update
+
+import android.annotation.SuppressLint
+import java.io.File
+import java.io.IOException
+
+class DownloadTask(val url: String, cacheDir: File?, hash: String) {
+
+    val file: File by lazy { createTempFile(cacheDir, hash) }
+
+    // 此次下载的起始位置
+    val bytesStart: Long by lazy { if (file.exists()) file.length() else 0 }
+    // 文件总大小
+    var bytesTotal: Long = 0
+        private set
+    // 此次下载的字节数
+    var bytesCopied: Long = 0
+        private set
+
+    var timeStart: Long = 0
+        private set
+    var timeUsed: Long = 1
+        private set
+    var timeLast: Long = 0
+        private set
+
+    var speed: Long = 0
+        private set
+
+    private val listeners: MutableSet<DownloadListener> = mutableSetOf()
+
+    @Volatile
+    var isCancelled: Boolean = false
+
+
+    @SuppressLint("UsableSpace")
+    fun isDiskNoSpace(): Boolean {
+        val storage = file.parentFile?.usableSpace ?: 0L
+        return bytesTotal - bytesStart > storage
+    }
+
+    fun addListener(listener: DownloadListener) {
+        listeners.add(listener)
+    }
+
+
+    fun start(total: Long) {
+        bytesTotal = total
+        timeStart = System.currentTimeMillis()
+        listeners.forEach { it.onStart() }
+    }
+
+    fun progress(bytes: Long, now: Long) {
+        bytesCopied += bytes
+        if (now - timeLast > 900) {
+            timeLast = now
+            timeUsed = now - timeStart
+            speed = bytesCopied * 1000 / timeUsed
+
+            val progress = 100f * (bytesCopied + bytesStart) / bytesTotal
+
+            listeners.forEach { it.onProgress(progress) }
+        }
+    }
+
+    fun finish() {
+        listeners.forEach { it.onFinish() }
+    }
+
+
+    private fun createTempFile(cacheDir: File?, hash: String): File {
+        val file = File(cacheDir, hash)
+        val oldHash = UpdateStore.updateHash
+        if (hash != oldHash) {
+            UpdateStore.updateHash = hash
+
+            // delete old temp file
+            if (!oldHash.isNullOrEmpty()) {
+                val oldFile = File(cacheDir, oldHash)
+                if (oldFile.exists()) {
+                    oldFile.delete()
+                }
+            }
+        }
+        // create new temp file
+        if (!file.exists()) {
+            try {
+                file.createNewFile()
+            } catch (e: IOException) {
+                e.printStackTrace()
+            }
+        }
+        return file
+    }
+}

+ 125 - 0
app/src/main/java/com/rdiot/yx485/net/update/HttpUtil.kt

@@ -0,0 +1,125 @@
+package com.rdiot.yx485.net.update
+
+import androidx.annotation.WorkerThread
+import java.io.BufferedInputStream
+import java.io.FileNotFoundException
+import java.io.IOException
+import java.io.RandomAccessFile
+import java.net.HttpURLConnection
+import java.net.URL
+
+object HttpUtil {
+
+    @WorkerThread
+    fun query(url: String, postData: ByteArray? = null, headers: Map<String, String> = mapOf()): String {
+        var connection: HttpURLConnection? = null
+        try {
+            connection = URL(url).openConnection() as HttpURLConnection
+            connection.setRequestProperty("Accept", "application/json")
+            if (postData == null) {
+                connection.requestMethod = "GET"
+                headers.forEach {
+                    connection.setRequestProperty(it.key, it.value)
+                }
+                connection.connect()
+            } else {
+                connection.requestMethod = "POST"
+                connection.doOutput = true
+                connection.instanceFollowRedirects = false
+                connection.useCaches = false
+                headers.forEach {
+                    connection.setRequestProperty(it.key, it.value)
+                }
+                connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
+                connection.setRequestProperty("Content-Length", postData.size.toString())
+                connection.outputStream.write(postData)
+            }
+            if (connection.responseCode != HttpURLConnection.HTTP_OK) {
+                throw UpdateResult(UpdateResult.CHECK_HTTP_STATUS, connection.responseCode.toString())
+            }
+            return String(connection.inputStream.readBytes())
+        } catch (ex: IOException) {
+            ex.printStackTrace()
+            throw UpdateResult(UpdateResult.CHECK_NETWORK_IO)
+        } finally {
+            connection?.disconnect()
+        }
+    }
+
+
+    @WorkerThread
+    fun download(task: DownloadTask) {
+        var connection: HttpURLConnection? = null
+        try {
+            connection = connect(task.url)
+
+            task.start(connection.contentLength.toLong())
+
+            if (task.bytesTotal == task.bytesStart) {
+                throw UpdateResult()
+            }
+            if (task.isDiskNoSpace()) {
+                throw UpdateResult(UpdateResult.DOWNLOAD_DISK_NO_SPACE)
+            }
+            if (task.bytesStart > 0) {
+                connection.disconnect()
+                connection = connect(task.url, mapOf("Range" to "bytes=${task.bytesStart}-"))
+            }
+
+            connection.inputStream.buffered(1024 * 100).copyTo(task)
+
+            if (task.isCancelled) {
+                throw UpdateResult(UpdateResult.UPDATE_CANCELLED)
+            }
+            if (task.bytesStart + task.bytesCopied != task.bytesTotal && task.bytesTotal != -1L) {
+                throw UpdateResult(UpdateResult.DOWNLOAD_INCOMPLETE)
+            }
+        } catch (e: FileNotFoundException) {
+            e.printStackTrace()
+            throw UpdateResult(UpdateResult.DOWNLOAD_DISK_IO)
+        } catch (e: IOException) {
+            e.printStackTrace()
+            throw UpdateResult(UpdateResult.DOWNLOAD_NETWORK_IO)
+        } finally {
+            connection?.disconnect()
+            task.finish()
+        }
+    }
+
+    private fun connect(url: String, headers: Map<String, String> = mapOf()): HttpURLConnection {
+        val connection = URL(url).openConnection() as HttpURLConnection
+        connection.setRequestProperty("Accept", "application/*")
+        headers.forEach {
+            connection.setRequestProperty(it.key, it.value)
+        }
+        connection.connectTimeout = 10000
+        connection.connect()
+
+        if (connection.responseCode != 200 && connection.responseCode != 206) {
+            throw UpdateResult(UpdateResult.DOWNLOAD_HTTP_STATUS, "${connection.responseCode}")
+        }
+        return connection
+    }
+
+    private fun BufferedInputStream.copyTo(task: DownloadTask) = use {
+        val buffer = ByteArray(1024 * 100)
+        RandomAccessFile(task.file, "rw").use { out ->
+            out.seek(out.length())
+            var previousBlockTime: Long = -1
+            while (!task.isCancelled) {
+                val bytes = it.read(buffer)
+                if (bytes == -1) {
+                    break
+                }
+                out.write(buffer, 0, bytes)
+                val now = System.currentTimeMillis()
+                task.progress(bytes.toLong(), now)
+                when {
+                    task.speed != 0L -> previousBlockTime = -1L
+                    previousBlockTime == -1L -> previousBlockTime = now
+                    System.currentTimeMillis() - previousBlockTime > 30000 -> throw UpdateResult(UpdateResult.DOWNLOAD_NETWORK_TIMEOUT)
+                }
+            }
+        }
+    }
+}

+ 63 - 0
app/src/main/java/com/rdiot/yx485/net/update/NotificationDownloadListener.kt

@@ -0,0 +1,63 @@
+package com.rdiot.yx485.net.update
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import com.rdiot.yx485.R
+
+class NotificationDownloadListener(
+    private val context: Context,
+    private val channelId: String = "update",
+    private val notifyId: Int = 998
+) : DownloadListener {
+    private var builder: NotificationCompat.Builder? = null
+
+    override fun onStart() {
+        if (builder == null) {
+            val title =
+                context.getString(context.applicationInfo.labelRes) + context.getString(R.string.downloading)
+            builder = NotificationCompat.Builder(context, channelId)
+                .setOngoing(true)
+                .setAutoCancel(false)
+                .setSmallIcon(context.applicationInfo.icon)
+                .setTicker(title)
+                .setContentTitle(title)
+        }
+        onProgress(0f)
+    }
+
+    override fun onProgress(progress: Float) {
+        builder?.let {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                val channel = NotificationChannel(
+                    channelId,
+                    "Download progress",
+                    NotificationManager.IMPORTANCE_DEFAULT
+                )
+                notificationManager.createNotificationChannel(channel)
+            }
+            updateChannel(progress == 0f)
+            it.setDefaults(if (progress > 0f) 0 else Notification.DEFAULT_VIBRATE)
+            it.setProgress(100, progress.toInt(), false)
+            notificationManager.notify(notifyId, it.build())
+        }
+    }
+
+    override fun onFinish() {
+        notificationManager.cancel(notifyId)
+    }
+
+    private fun updateChannel(first: Boolean) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            val importance =
+                if (first) NotificationManager.IMPORTANCE_DEFAULT else NotificationManager.IMPORTANCE_LOW
+            val channel = NotificationChannel(channelId, "this is channel title", importance)
+            notificationManager.createNotificationChannel(channel)
+        }
+    }
+
+    private val notificationManager: NotificationManager get() = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+}

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor