search_device.dart 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:math';
  4. import 'package:android_intent/android_intent.dart';
  5. import 'package:flutter/cupertino.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter_blue/flutter_blue.dart';
  8. import 'package:provider/provider.dart';
  9. import 'package:sport/pages/my/device_info_page.dart';
  10. import 'package:sport/provider/bluetooth.dart';
  11. import 'package:sport/widgets/button_cancel.dart';
  12. import 'package:sport/widgets/button_primary.dart';
  13. import 'package:sport/widgets/image.dart';
  14. import 'package:sport/widgets/misc.dart';
  15. import 'package:url_launcher/url_launcher.dart';
  16. class SearchDeviceDialog extends StatefulWidget {
  17. @override
  18. State<StatefulWidget> createState() => _SearchDeviceDialog();
  19. }
  20. class _SearchDeviceDialog extends State<SearchDeviceDialog> {
  21. @override
  22. void dispose() {
  23. super.dispose();
  24. FlutterBlue.instance.stopScan();
  25. }
  26. @override
  27. Widget build(BuildContext context) {
  28. return Dialog(
  29. elevation: 0,
  30. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
  31. child: Padding(
  32. padding: const EdgeInsets.all(8.0),
  33. child: ConstrainedBox(
  34. constraints: BoxConstraints(maxHeight: 350, minWidth: double.infinity),
  35. child: Column(
  36. children: <Widget>[
  37. Stack(
  38. alignment: Alignment.center,
  39. children: <Widget>[
  40. Center(
  41. child: Padding(
  42. padding: const EdgeInsets.all(6.0),
  43. child: Text("请选择鞋子", style: Theme.of(context).textTheme.headline3),
  44. ),
  45. ),
  46. Positioned(
  47. right: 0,
  48. top: 0,
  49. child: GestureDetector(
  50. behavior: HitTestBehavior.opaque,
  51. onTap: () => Navigator.pop(context),
  52. child: Padding(
  53. padding: const EdgeInsets.all(6.0),
  54. child: Image.asset("lib/assets/img/btn_close_big.png"),
  55. ),
  56. ),
  57. ),
  58. ],
  59. ),
  60. Divider(),
  61. Expanded(
  62. child: StreamBuilder<BluetoothState>(
  63. stream: FlutterBlue.instance.state,
  64. initialData: BluetoothState.unknown,
  65. builder: (c, snapshot) {
  66. final state = snapshot.data;
  67. if (state == BluetoothState.on) {
  68. return FindDevicesScreen();
  69. }
  70. return BluetoothOffScreen(state: state);
  71. }),
  72. ),
  73. ],
  74. ),
  75. )),
  76. );
  77. }
  78. }
  79. class BluetoothOffScreen extends StatelessWidget {
  80. const BluetoothOffScreen({Key key, this.state}) : super(key: key);
  81. final BluetoothState state;
  82. @override
  83. Widget build(BuildContext context) {
  84. return Column(
  85. mainAxisSize: MainAxisSize.min,
  86. children: <Widget>[
  87. SizedBox(
  88. height: 60,
  89. ),
  90. Image.asset("lib/assets/img/pop_image_close.png"),
  91. SizedBox(
  92. height: 20,
  93. ),
  94. Text(
  95. '搜索失败,请确认蓝牙是否打开',
  96. style: Theme.of(context).textTheme.bodyText2.copyWith(color: Color(0xff666666)),
  97. ),
  98. SizedBox(
  99. height: 10,
  100. ),
  101. GestureDetector(
  102. behavior: HitTestBehavior.opaque,
  103. onTap: () async {
  104. if (Platform.isAndroid) {
  105. AndroidIntent intent = AndroidIntent(action: "android.bluetooth.adapter.action.REQUEST_ENABLE");
  106. await intent.launch();
  107. } else if (Platform.isIOS) {
  108. launch("App-Prefs:root=Bluetooth ");
  109. }
  110. },
  111. child: Center(
  112. child: Row(
  113. mainAxisSize: MainAxisSize.min,
  114. children: <Widget>[
  115. Text(
  116. '打开蓝牙设置',
  117. style: Theme.of(context).textTheme.bodyText2.copyWith(color: Theme.of(context).accentColor),
  118. ),
  119. SizedBox(
  120. width: 6,
  121. ),
  122. arrowRight6()
  123. ],
  124. ),
  125. ),
  126. ),
  127. ],
  128. );
  129. }
  130. }
  131. class FindDevicesScreen extends StatefulWidget {
  132. @override
  133. State<StatefulWidget> createState() => _FindDevicesScreen();
  134. }
  135. class _FindDevicesScreen extends State<FindDevicesScreen> with SingleTickerProviderStateMixin {
  136. AnimationController _animationController;
  137. Animation _animation;
  138. bool _search = true;
  139. StreamSubscription streamSubscription;
  140. @override
  141. void initState() {
  142. _animationController = AnimationController(duration: Duration(seconds: 2), vsync: this);
  143. _animation = Tween(begin: .0, end: 1.0).animate(_animationController)
  144. ..addStatusListener((status) {
  145. if (status == AnimationStatus.completed) {
  146. _animationController.repeat();
  147. }
  148. });
  149. //开始动画
  150. _animationController.forward();
  151. super.initState();
  152. startScan();
  153. streamSubscription = Provider.of<Bluetooth>(context, listen: false).stateStream.listen((event) async {
  154. await Future.delayed(Duration(seconds: 2));
  155. Navigator.pop(context);
  156. });
  157. }
  158. startScan() async {
  159. setState(() {
  160. _search = true;
  161. });
  162. FlutterBlue.instance.startScan(timeout: Duration(seconds: 10)).whenComplete(() {
  163. if (mounted)
  164. setState(() {
  165. _search = false;
  166. });
  167. });
  168. }
  169. @override
  170. void dispose() {
  171. _animationController?.dispose();
  172. super.dispose();
  173. streamSubscription?.cancel();
  174. }
  175. @override
  176. Widget build(BuildContext context) {
  177. return Column(
  178. children: <Widget>[
  179. Expanded(
  180. child: SingleChildScrollView(
  181. child: Column(
  182. children: <Widget>[
  183. // if (!_search)
  184. // StreamBuilder<List<BluetoothDevice>>(
  185. // stream: Stream.fromFuture(FlutterBlue.instance.connectedDevices),
  186. // initialData: [],
  187. // builder: (c, snapshot) => Column(
  188. // children: snapshot.data
  189. // .map((d) => Column(
  190. // children: <Widget>[
  191. // ListTile(
  192. // title: Column(
  193. // mainAxisAlignment: MainAxisAlignment.start,
  194. // crossAxisAlignment: CrossAxisAlignment.start,
  195. // children: <Widget>[
  196. // Text(
  197. // d.name,
  198. // overflow: TextOverflow.ellipsis,
  199. // style: Theme.of(context).textTheme.headline3,
  200. // ),
  201. // SizedBox(
  202. // height: 4,
  203. // ),
  204. // Text(
  205. // d.id.toString(),
  206. // style: Theme.of(context).textTheme.caption,
  207. // ),
  208. // ],
  209. // ),
  210. // trailing: StreamBuilder<BluetoothDeviceState>(
  211. // stream: d.state,
  212. // initialData: BluetoothDeviceState.disconnected,
  213. // builder: (c, snapshot) {
  214. // return GestureDetector(
  215. // onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => DeviceInfoPage())),
  216. // child: Container(
  217. // width: 58,
  218. // height: 30,
  219. // alignment: Alignment.center,
  220. // decoration: BoxDecoration(
  221. // shape: BoxShape.rectangle,
  222. // borderRadius: BorderRadius.all(Radius.circular(100)),
  223. // color: Theme.of(context).accentColor),
  224. // child: Text(
  225. // snapshot.data == BluetoothDeviceState.connected ? "已连接" : "未连接",
  226. // style: TextStyle(color: Colors.white, fontSize: 12),
  227. // ),
  228. // ),
  229. // );
  230. // },
  231. // ),
  232. // ),
  233. // Divider()
  234. // ],
  235. // ))
  236. // .toList(),
  237. // ),
  238. // ),
  239. // if (!_search)
  240. StreamBuilder<List<ScanResult>>(
  241. stream: FlutterBlue.instance.scanResults,
  242. initialData: [],
  243. builder: (c, snapshot) {
  244. var bluetooth = Provider.of<Bluetooth>(context, listen: false);
  245. var list = snapshot.data.where((element) => element.device.name.isNotEmpty).toList();
  246. list.sort((a, b) => bluetooth.device == a.device ? -1 : 1);
  247. return Column(
  248. children: list.isEmpty == true
  249. ? [
  250. _search
  251. ? Padding(
  252. padding: const EdgeInsets.symmetric(vertical: 30),
  253. child: Column(
  254. children: <Widget>[
  255. Stack(
  256. alignment: Alignment.center,
  257. children: <Widget>[
  258. RotationTransition(turns: _animation, child: Image.asset("lib/assets/img/pop_image_circle.png")),
  259. Image.asset("lib/assets/img/pop_image_shoes.png")
  260. ],
  261. ),
  262. SizedBox(
  263. height: 12,
  264. ),
  265. Text("搜索中...")
  266. ],
  267. ),
  268. )
  269. : Padding(
  270. padding: const EdgeInsets.symmetric(horizontal: 30.0,vertical: 65.0),
  271. child: Center(
  272. child: Column(
  273. children: <Widget>[
  274. Image.asset("lib/assets/img/pop_image_noequipment.png"),
  275. SizedBox(
  276. height: 10,
  277. ),
  278. Text("暂无设备")
  279. ],
  280. ),
  281. ))
  282. ]
  283. : list
  284. .map(
  285. (r) => Column(
  286. children: <Widget>[
  287. ScanResultTile(
  288. result: r,
  289. onTap: () async {
  290. await Provider.of<Bluetooth>(context, listen: false).saveDevice(r.device);
  291. setState(() {});
  292. },
  293. ),
  294. Divider()
  295. ],
  296. ),
  297. )
  298. .toList(),
  299. );
  300. },
  301. ),
  302. ],
  303. ),
  304. ),
  305. ),
  306. Padding(
  307. padding: const EdgeInsets.all(8.0),
  308. child: StreamBuilder<bool>(
  309. stream: FlutterBlue.instance.isScanning,
  310. initialData: false,
  311. builder: (c, snapshot) {
  312. if (snapshot.data) {
  313. return CancelButton(
  314. height: 35,
  315. content: "搜索中...",
  316. callback: () {
  317. FlutterBlue.instance.stopScan();
  318. setState(() {
  319. _search = false;
  320. });
  321. },
  322. );
  323. } else {
  324. return PrimaryButton(height: 35, content: "重新搜索", callback: () => startScan());
  325. }
  326. },
  327. ),
  328. ),
  329. ],
  330. );
  331. }
  332. }
  333. class DeviceScreen extends StatelessWidget {
  334. const DeviceScreen({Key key, this.device}) : super(key: key);
  335. final BluetoothDevice device;
  336. List<int> _getRandomBytes() {
  337. final math = Random();
  338. return [math.nextInt(255), math.nextInt(255), math.nextInt(255), math.nextInt(255)];
  339. }
  340. List<Widget> _buildServiceTiles(List<BluetoothService> services) {
  341. return services
  342. .map(
  343. (s) => ServiceTile(
  344. service: s,
  345. characteristicTiles: s.characteristics
  346. .map(
  347. (c) => CharacteristicTile(
  348. characteristic: c,
  349. onReadPressed: () => c.read(),
  350. onWritePressed: () async {
  351. await c.write(_getRandomBytes(), withoutResponse: true);
  352. await c.read();
  353. },
  354. onNotificationPressed: () async {
  355. await c.setNotifyValue(!c.isNotifying);
  356. c.value.listen((value) {
  357. print(value.length);
  358. });
  359. },
  360. descriptorTiles: c.descriptors
  361. .map(
  362. (d) => DescriptorTile(
  363. descriptor: d,
  364. onReadPressed: () => d.read(),
  365. onWritePressed: () => d.write(_getRandomBytes()),
  366. ),
  367. )
  368. .toList(),
  369. ),
  370. )
  371. .toList(),
  372. ),
  373. )
  374. .toList();
  375. }
  376. @override
  377. Widget build(BuildContext context) {
  378. return Scaffold(
  379. appBar: AppBar(
  380. title: Text(device.name),
  381. actions: <Widget>[
  382. StreamBuilder<BluetoothDeviceState>(
  383. stream: device.state,
  384. initialData: BluetoothDeviceState.connecting,
  385. builder: (c, snapshot) {
  386. VoidCallback onPressed;
  387. String text;
  388. switch (snapshot.data) {
  389. case BluetoothDeviceState.connected:
  390. onPressed = () => device.disconnect();
  391. text = 'DISCONNECT';
  392. break;
  393. case BluetoothDeviceState.disconnected:
  394. onPressed = () => device.connect();
  395. text = 'CONNECT';
  396. break;
  397. default:
  398. onPressed = null;
  399. text = snapshot.data.toString().substring(21).toUpperCase();
  400. break;
  401. }
  402. return FlatButton(
  403. onPressed: onPressed,
  404. child: Text(
  405. text,
  406. style: Theme.of(context).primaryTextTheme.button.copyWith(color: Colors.white),
  407. ));
  408. },
  409. )
  410. ],
  411. ),
  412. body: SingleChildScrollView(
  413. child: Column(
  414. children: <Widget>[
  415. StreamBuilder<BluetoothDeviceState>(
  416. stream: device.state,
  417. initialData: BluetoothDeviceState.connecting,
  418. builder: (c, snapshot) => ListTile(
  419. leading: (snapshot.data == BluetoothDeviceState.connected) ? Icon(Icons.bluetooth_connected) : Icon(Icons.bluetooth_disabled),
  420. title: Text('Device is ${snapshot.data.toString().split('.')[1]}.'),
  421. subtitle: Text('${device.id}'),
  422. trailing: StreamBuilder<bool>(
  423. stream: device.isDiscoveringServices,
  424. initialData: false,
  425. builder: (c, snapshot) => IndexedStack(
  426. index: snapshot.data ? 1 : 0,
  427. children: <Widget>[
  428. IconButton(
  429. icon: Icon(Icons.refresh),
  430. onPressed: () => device.discoverServices(),
  431. ),
  432. IconButton(
  433. icon: SizedBox(
  434. child: CircularProgressIndicator(
  435. valueColor: AlwaysStoppedAnimation(Colors.grey),
  436. ),
  437. width: 18.0,
  438. height: 18.0,
  439. ),
  440. onPressed: null,
  441. )
  442. ],
  443. ),
  444. ),
  445. ),
  446. ),
  447. StreamBuilder<int>(
  448. stream: device.mtu,
  449. initialData: 0,
  450. builder: (c, snapshot) => ListTile(
  451. title: Text('MTU Size'),
  452. subtitle: Text('${snapshot.data} bytes'),
  453. trailing: IconButton(
  454. icon: Icon(Icons.edit),
  455. onPressed: () => device.requestMtu(223),
  456. ),
  457. ),
  458. ),
  459. StreamBuilder<List<BluetoothService>>(
  460. stream: device.services,
  461. initialData: [],
  462. builder: (c, snapshot) {
  463. return Column(
  464. children: _buildServiceTiles(snapshot.data),
  465. );
  466. },
  467. ),
  468. RaisedButton(
  469. onPressed: () {
  470. Provider.of<Bluetooth>(context, listen: false).queryDeviceStep();
  471. },
  472. child: Text("同步步数"),
  473. ),
  474. RaisedButton(
  475. onPressed: () {
  476. Provider.of<Bluetooth>(context, listen: false).resetData();
  477. },
  478. child: Text("清零"),
  479. ),
  480. RaisedButton(
  481. onPressed: () {
  482. Provider.of<Bluetooth>(context, listen: false).setupGameMode(true);
  483. },
  484. child: Text("游戏模式开"),
  485. ),
  486. RaisedButton(
  487. onPressed: () {
  488. Provider.of<Bluetooth>(context, listen: false).setupGameMode(false);
  489. },
  490. child: Text("游戏模式关"),
  491. ),
  492. RaisedButton(
  493. onPressed: () {
  494. Provider.of<Bluetooth>(context, listen: false).vibrate(Provider.of<Bluetooth>(context, listen: false).vibrateNotifier.value);
  495. },
  496. child: Text("发送震动"),
  497. ),
  498. ValueListenableBuilder(
  499. valueListenable: Provider.of<Bluetooth>(context, listen: false).vibrateNotifier,
  500. builder: (BuildContext context, int v, Widget child) => Row(
  501. children: <Widget>[
  502. Slider(
  503. divisions: 9,
  504. value: v.toDouble(),
  505. min: 100,
  506. max: 1000,
  507. onChanged: (double value) {
  508. Provider.of<Bluetooth>(context, listen: false).vibrateNotifier.value = value.toInt();
  509. },
  510. ),
  511. Text("$v")
  512. ],
  513. ),
  514. ),
  515. ValueListenableBuilder(
  516. valueListenable: Provider.of<Bluetooth>(context, listen: false).actionNotifier,
  517. builder: (BuildContext context, int value, Widget child) => Text("当前动作: $value"),
  518. ),
  519. ValueListenableBuilder(
  520. valueListenable: Provider.of<Bluetooth>(context, listen: false).stepTotalNotifier,
  521. builder: (BuildContext context, int value, Widget child) => Text("同步步数: $value"),
  522. ),
  523. ValueListenableBuilder(
  524. valueListenable: Provider.of<Bluetooth>(context, listen: false).stepNotifier,
  525. builder: (BuildContext context, int value, Widget child) => Text("相对步数: $value"),
  526. ),
  527. ValueListenableBuilder(
  528. valueListenable: Provider.of<Bluetooth>(context, listen: false).byteNotifier,
  529. builder: (BuildContext context, List<int> value, Widget child) => Text("接收: $value"),
  530. ),
  531. ],
  532. ),
  533. ),
  534. );
  535. }
  536. }
  537. class ScanResultTile extends StatelessWidget {
  538. const ScanResultTile({Key key, this.result, this.onTap}) : super(key: key);
  539. final ScanResult result;
  540. final VoidCallback onTap;
  541. Widget _buildTitle(BuildContext context) {
  542. if (result.device.name.length > 0) {
  543. return Column(
  544. mainAxisAlignment: MainAxisAlignment.start,
  545. crossAxisAlignment: CrossAxisAlignment.start,
  546. children: <Widget>[
  547. Text(
  548. result.device.name,
  549. overflow: TextOverflow.ellipsis,
  550. style: Theme.of(context).textTheme.headline3,
  551. ),
  552. SizedBox(
  553. height: 4,
  554. ),
  555. Text(
  556. result.device.id.toString(),
  557. style: Theme.of(context).textTheme.caption,
  558. ),
  559. ],
  560. );
  561. } else {
  562. return Text(result.device.id.toString());
  563. }
  564. }
  565. Widget _buildAdvRow(BuildContext context, String title, String value) {
  566. return Padding(
  567. padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0),
  568. child: Row(
  569. crossAxisAlignment: CrossAxisAlignment.start,
  570. children: <Widget>[
  571. Text(title, style: Theme.of(context).textTheme.caption),
  572. SizedBox(
  573. width: 12.0,
  574. ),
  575. Expanded(
  576. child: Text(
  577. value,
  578. style: Theme.of(context).textTheme.caption.apply(color: Colors.black),
  579. softWrap: true,
  580. ),
  581. ),
  582. ],
  583. ),
  584. );
  585. }
  586. String getNiceHexArray(List<int> bytes) {
  587. return '[${bytes.map((i) => i.toRadixString(16).padLeft(2, '0')).join(', ')}]'.toUpperCase();
  588. }
  589. String getNiceManufacturerData(Map<int, List<int>> data) {
  590. if (data.isEmpty) {
  591. return null;
  592. }
  593. List<String> res = [];
  594. data.forEach((id, bytes) {
  595. res.add('${id.toRadixString(16).toUpperCase()}: ${getNiceHexArray(bytes)}');
  596. });
  597. return res.join(', ');
  598. }
  599. String getNiceServiceData(Map<String, List<int>> data) {
  600. if (data.isEmpty) {
  601. return null;
  602. }
  603. List<String> res = [];
  604. data.forEach((id, bytes) {
  605. res.add('${id.toUpperCase()}: ${getNiceHexArray(bytes)}');
  606. });
  607. return res.join(', ');
  608. }
  609. @override
  610. Widget build(BuildContext context) {
  611. return ListTile(
  612. contentPadding: EdgeInsets.symmetric(horizontal: 12),
  613. title: _buildTitle(context),
  614. // leading: Text(result.rssi.toString()),
  615. trailing: StreamBuilder<BluetoothDeviceState>(
  616. stream: result.device.state,
  617. initialData: BluetoothDeviceState.connecting,
  618. builder: (c, snapshot) {
  619. if (snapshot.data == BluetoothDeviceState.connected) {
  620. return Row(
  621. mainAxisSize: MainAxisSize.min,
  622. children: <Widget>[
  623. Image.asset(
  624. "lib/assets/img/pop_icon_conneted.png",
  625. width: 14.0,
  626. ),
  627. SizedBox(
  628. width: 4,
  629. ),
  630. Text(
  631. "已连接",
  632. style: TextStyle(
  633. color: Theme.of(context).accentColor,
  634. fontSize: 12,
  635. ),
  636. strutStyle: fixedLine,
  637. )
  638. ],
  639. );
  640. }
  641. int status = Provider.of<Bluetooth>(context, listen: false).device == result.device
  642. ? 1
  643. : snapshot.data == BluetoothDeviceState.disconnected
  644. ? 0
  645. : snapshot.data == BluetoothDeviceState.connecting ? 1 : snapshot.data == BluetoothDeviceState.connected ? 2 : 3;
  646. return status == 1
  647. ? Padding(
  648. padding: const EdgeInsets.symmetric(horizontal: 4.0),
  649. child: Text(
  650. "正在连接...",
  651. style: TextStyle(
  652. color: Theme.of(context).accentColor,
  653. fontSize: 12,
  654. ),
  655. strutStyle: fixedLine,
  656. ),
  657. )
  658. : GestureDetector(
  659. onTap: onTap,
  660. child: Container(
  661. width: 64,
  662. height: 30,
  663. alignment: Alignment.center,
  664. decoration: BoxDecoration(
  665. borderRadius: BorderRadius.all(Radius.circular(100)),
  666. border: Border.all(
  667. color: Theme.of(context).accentColor,
  668. )),
  669. child: Center(
  670. child: Row(
  671. mainAxisSize: MainAxisSize.min,
  672. children: <Widget>[
  673. Text(
  674. status == 1 ? "连接中" : status == 0 ? "连接" : "已连接",
  675. style: TextStyle(
  676. color: Theme.of(context).accentColor,
  677. fontSize: 12,
  678. ),
  679. strutStyle: fixedLine,
  680. )
  681. ],
  682. ),
  683. ),
  684. ),
  685. );
  686. },
  687. ),
  688. // children: <Widget>[
  689. // _buildAdvRow(context, 'Complete Local Name', result.advertisementData.localName),
  690. // _buildAdvRow(context, 'Tx Power Level', '${result.advertisementData.txPowerLevel ?? 'N/A'}'),
  691. // _buildAdvRow(context, 'Manufacturer Data', getNiceManufacturerData(result.advertisementData.manufacturerData) ?? 'N/A'),
  692. // _buildAdvRow(context, 'Service UUIDs',
  693. // (result.advertisementData.serviceUuids.isNotEmpty) ? result.advertisementData.serviceUuids.join(', ').toUpperCase() : 'N/A'),
  694. // _buildAdvRow(context, 'Service Data', getNiceServiceData(result.advertisementData.serviceData) ?? 'N/A'),
  695. // ],
  696. );
  697. }
  698. }
  699. class ServiceTile extends StatelessWidget {
  700. final BluetoothService service;
  701. final List<CharacteristicTile> characteristicTiles;
  702. const ServiceTile({Key key, this.service, this.characteristicTiles}) : super(key: key);
  703. @override
  704. Widget build(BuildContext context) {
  705. if (characteristicTiles.length > 0) {
  706. return ExpansionTile(
  707. title: Column(
  708. mainAxisAlignment: MainAxisAlignment.center,
  709. crossAxisAlignment: CrossAxisAlignment.start,
  710. children: <Widget>[
  711. Text('Service'),
  712. Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}',
  713. style: Theme.of(context).textTheme.body1.copyWith(color: Theme.of(context).textTheme.caption.color))
  714. ],
  715. ),
  716. children: characteristicTiles,
  717. );
  718. } else {
  719. return ListTile(
  720. title: Text('Service'),
  721. subtitle: Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}'),
  722. );
  723. }
  724. }
  725. }
  726. class CharacteristicTile extends StatelessWidget {
  727. final BluetoothCharacteristic characteristic;
  728. final List<DescriptorTile> descriptorTiles;
  729. final VoidCallback onReadPressed;
  730. final VoidCallback onWritePressed;
  731. final VoidCallback onNotificationPressed;
  732. const CharacteristicTile({Key key, this.characteristic, this.descriptorTiles, this.onReadPressed, this.onWritePressed, this.onNotificationPressed})
  733. : super(key: key);
  734. @override
  735. Widget build(BuildContext context) {
  736. return StreamBuilder<List<int>>(
  737. stream: characteristic.value,
  738. initialData: characteristic.lastValue,
  739. builder: (c, snapshot) {
  740. final value = snapshot.data;
  741. return ExpansionTile(
  742. title: ListTile(
  743. title: Column(
  744. mainAxisAlignment: MainAxisAlignment.center,
  745. crossAxisAlignment: CrossAxisAlignment.start,
  746. children: <Widget>[
  747. Text('Characteristic'),
  748. Text('0x${characteristic.uuid.toString().toUpperCase().substring(4, 8)}',
  749. style: Theme.of(context).textTheme.body1.copyWith(color: Theme.of(context).textTheme.caption.color))
  750. ],
  751. ),
  752. subtitle: Text(value.toString()),
  753. contentPadding: EdgeInsets.all(0.0),
  754. ),
  755. trailing: Row(
  756. mainAxisSize: MainAxisSize.min,
  757. children: <Widget>[
  758. IconButton(
  759. icon: Icon(
  760. Icons.file_download,
  761. color: Theme.of(context).iconTheme.color.withOpacity(0.5),
  762. ),
  763. onPressed: onReadPressed,
  764. ),
  765. IconButton(
  766. icon: Icon(Icons.file_upload, color: Theme.of(context).iconTheme.color.withOpacity(0.5)),
  767. onPressed: onWritePressed,
  768. ),
  769. IconButton(
  770. icon: Icon(characteristic.isNotifying ? Icons.sync_disabled : Icons.sync, color: Theme.of(context).iconTheme.color.withOpacity(0.5)),
  771. onPressed: onNotificationPressed,
  772. )
  773. ],
  774. ),
  775. children: descriptorTiles,
  776. );
  777. },
  778. );
  779. }
  780. }
  781. class DescriptorTile extends StatelessWidget {
  782. final BluetoothDescriptor descriptor;
  783. final VoidCallback onReadPressed;
  784. final VoidCallback onWritePressed;
  785. const DescriptorTile({Key key, this.descriptor, this.onReadPressed, this.onWritePressed}) : super(key: key);
  786. @override
  787. Widget build(BuildContext context) {
  788. return ListTile(
  789. title: Column(
  790. mainAxisAlignment: MainAxisAlignment.center,
  791. crossAxisAlignment: CrossAxisAlignment.start,
  792. children: <Widget>[
  793. Text('Descriptor'),
  794. Text('0x${descriptor.uuid.toString().toUpperCase().substring(4, 8)}',
  795. style: Theme.of(context).textTheme.body1.copyWith(color: Theme.of(context).textTheme.caption.color))
  796. ],
  797. ),
  798. subtitle: StreamBuilder<List<int>>(
  799. stream: descriptor.value,
  800. initialData: descriptor.lastValue,
  801. builder: (c, snapshot) => Text(snapshot.data.toString()),
  802. ),
  803. trailing: Row(
  804. mainAxisSize: MainAxisSize.min,
  805. children: <Widget>[
  806. IconButton(
  807. icon: Icon(
  808. Icons.file_download,
  809. color: Theme.of(context).iconTheme.color.withOpacity(0.5),
  810. ),
  811. onPressed: onReadPressed,
  812. ),
  813. IconButton(
  814. icon: Icon(
  815. Icons.file_upload,
  816. color: Theme.of(context).iconTheme.color.withOpacity(0.5),
  817. ),
  818. onPressed: onWritePressed,
  819. )
  820. ],
  821. ),
  822. );
  823. }
  824. }
  825. class AdapterStateTile extends StatelessWidget {
  826. const AdapterStateTile({Key key, @required this.state}) : super(key: key);
  827. final BluetoothState state;
  828. @override
  829. Widget build(BuildContext context) {
  830. return Container(
  831. color: Colors.redAccent,
  832. child: ListTile(
  833. title: Text(
  834. 'Bluetooth adapter is ${state.toString().substring(15)}',
  835. style: Theme.of(context).primaryTextTheme.subhead,
  836. ),
  837. trailing: Icon(
  838. Icons.error,
  839. color: Theme.of(context).primaryTextTheme.subhead.color,
  840. ),
  841. ),
  842. );
  843. }
  844. }