search_device.dart 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  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.all(30.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 Container(
  621. width: 64,
  622. height: 30,
  623. alignment: Alignment.center,
  624. decoration: BoxDecoration(shape: BoxShape.rectangle, borderRadius: BorderRadius.all(Radius.circular(100)), color: Theme.of(context).accentColor),
  625. child: Text(
  626. snapshot.data == BluetoothDeviceState.connected ? "已连接" : "未连接",
  627. style: TextStyle(color: Colors.white, fontSize: 12),
  628. ),
  629. );
  630. }
  631. int status = Provider.of<Bluetooth>(context, listen: false).device == result.device
  632. ? 1
  633. : snapshot.data == BluetoothDeviceState.disconnected
  634. ? 0
  635. : snapshot.data == BluetoothDeviceState.connecting ? 1 : snapshot.data == BluetoothDeviceState.connected ? 2 : 3;
  636. return status == 1
  637. ? Padding(
  638. padding: const EdgeInsets.symmetric(horizontal: 4.0),
  639. child: SizedBox(
  640. width: 12,
  641. height: 12,
  642. child: CircularProgressIndicator(
  643. strokeWidth: 2,
  644. ),
  645. ),
  646. )
  647. : GestureDetector(
  648. onTap: onTap,
  649. child: Container(
  650. width: 64,
  651. height: 30,
  652. alignment: Alignment.center,
  653. decoration: BoxDecoration(
  654. borderRadius: BorderRadius.all(Radius.circular(100)),
  655. border: Border.all(
  656. color: Theme.of(context).accentColor,
  657. )),
  658. child: Center(
  659. child: Row(
  660. mainAxisSize: MainAxisSize.min,
  661. children: <Widget>[
  662. Text(
  663. status == 1 ? "连接中" : status == 0 ? "连接" : "已连接",
  664. style: TextStyle(
  665. color: Theme.of(context).accentColor,
  666. fontSize: 12,
  667. ),
  668. strutStyle: fixedLine,
  669. )
  670. ],
  671. ),
  672. ),
  673. ),
  674. );
  675. },
  676. ),
  677. // children: <Widget>[
  678. // _buildAdvRow(context, 'Complete Local Name', result.advertisementData.localName),
  679. // _buildAdvRow(context, 'Tx Power Level', '${result.advertisementData.txPowerLevel ?? 'N/A'}'),
  680. // _buildAdvRow(context, 'Manufacturer Data', getNiceManufacturerData(result.advertisementData.manufacturerData) ?? 'N/A'),
  681. // _buildAdvRow(context, 'Service UUIDs',
  682. // (result.advertisementData.serviceUuids.isNotEmpty) ? result.advertisementData.serviceUuids.join(', ').toUpperCase() : 'N/A'),
  683. // _buildAdvRow(context, 'Service Data', getNiceServiceData(result.advertisementData.serviceData) ?? 'N/A'),
  684. // ],
  685. );
  686. }
  687. }
  688. class ServiceTile extends StatelessWidget {
  689. final BluetoothService service;
  690. final List<CharacteristicTile> characteristicTiles;
  691. const ServiceTile({Key key, this.service, this.characteristicTiles}) : super(key: key);
  692. @override
  693. Widget build(BuildContext context) {
  694. if (characteristicTiles.length > 0) {
  695. return ExpansionTile(
  696. title: Column(
  697. mainAxisAlignment: MainAxisAlignment.center,
  698. crossAxisAlignment: CrossAxisAlignment.start,
  699. children: <Widget>[
  700. Text('Service'),
  701. Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}',
  702. style: Theme.of(context).textTheme.body1.copyWith(color: Theme.of(context).textTheme.caption.color))
  703. ],
  704. ),
  705. children: characteristicTiles,
  706. );
  707. } else {
  708. return ListTile(
  709. title: Text('Service'),
  710. subtitle: Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}'),
  711. );
  712. }
  713. }
  714. }
  715. class CharacteristicTile extends StatelessWidget {
  716. final BluetoothCharacteristic characteristic;
  717. final List<DescriptorTile> descriptorTiles;
  718. final VoidCallback onReadPressed;
  719. final VoidCallback onWritePressed;
  720. final VoidCallback onNotificationPressed;
  721. const CharacteristicTile({Key key, this.characteristic, this.descriptorTiles, this.onReadPressed, this.onWritePressed, this.onNotificationPressed})
  722. : super(key: key);
  723. @override
  724. Widget build(BuildContext context) {
  725. return StreamBuilder<List<int>>(
  726. stream: characteristic.value,
  727. initialData: characteristic.lastValue,
  728. builder: (c, snapshot) {
  729. final value = snapshot.data;
  730. return ExpansionTile(
  731. title: ListTile(
  732. title: Column(
  733. mainAxisAlignment: MainAxisAlignment.center,
  734. crossAxisAlignment: CrossAxisAlignment.start,
  735. children: <Widget>[
  736. Text('Characteristic'),
  737. Text('0x${characteristic.uuid.toString().toUpperCase().substring(4, 8)}',
  738. style: Theme.of(context).textTheme.body1.copyWith(color: Theme.of(context).textTheme.caption.color))
  739. ],
  740. ),
  741. subtitle: Text(value.toString()),
  742. contentPadding: EdgeInsets.all(0.0),
  743. ),
  744. trailing: Row(
  745. mainAxisSize: MainAxisSize.min,
  746. children: <Widget>[
  747. IconButton(
  748. icon: Icon(
  749. Icons.file_download,
  750. color: Theme.of(context).iconTheme.color.withOpacity(0.5),
  751. ),
  752. onPressed: onReadPressed,
  753. ),
  754. IconButton(
  755. icon: Icon(Icons.file_upload, color: Theme.of(context).iconTheme.color.withOpacity(0.5)),
  756. onPressed: onWritePressed,
  757. ),
  758. IconButton(
  759. icon: Icon(characteristic.isNotifying ? Icons.sync_disabled : Icons.sync, color: Theme.of(context).iconTheme.color.withOpacity(0.5)),
  760. onPressed: onNotificationPressed,
  761. )
  762. ],
  763. ),
  764. children: descriptorTiles,
  765. );
  766. },
  767. );
  768. }
  769. }
  770. class DescriptorTile extends StatelessWidget {
  771. final BluetoothDescriptor descriptor;
  772. final VoidCallback onReadPressed;
  773. final VoidCallback onWritePressed;
  774. const DescriptorTile({Key key, this.descriptor, this.onReadPressed, this.onWritePressed}) : super(key: key);
  775. @override
  776. Widget build(BuildContext context) {
  777. return ListTile(
  778. title: Column(
  779. mainAxisAlignment: MainAxisAlignment.center,
  780. crossAxisAlignment: CrossAxisAlignment.start,
  781. children: <Widget>[
  782. Text('Descriptor'),
  783. Text('0x${descriptor.uuid.toString().toUpperCase().substring(4, 8)}',
  784. style: Theme.of(context).textTheme.body1.copyWith(color: Theme.of(context).textTheme.caption.color))
  785. ],
  786. ),
  787. subtitle: StreamBuilder<List<int>>(
  788. stream: descriptor.value,
  789. initialData: descriptor.lastValue,
  790. builder: (c, snapshot) => Text(snapshot.data.toString()),
  791. ),
  792. trailing: Row(
  793. mainAxisSize: MainAxisSize.min,
  794. children: <Widget>[
  795. IconButton(
  796. icon: Icon(
  797. Icons.file_download,
  798. color: Theme.of(context).iconTheme.color.withOpacity(0.5),
  799. ),
  800. onPressed: onReadPressed,
  801. ),
  802. IconButton(
  803. icon: Icon(
  804. Icons.file_upload,
  805. color: Theme.of(context).iconTheme.color.withOpacity(0.5),
  806. ),
  807. onPressed: onWritePressed,
  808. )
  809. ],
  810. ),
  811. );
  812. }
  813. }
  814. class AdapterStateTile extends StatelessWidget {
  815. const AdapterStateTile({Key key, @required this.state}) : super(key: key);
  816. final BluetoothState state;
  817. @override
  818. Widget build(BuildContext context) {
  819. return Container(
  820. color: Colors.redAccent,
  821. child: ListTile(
  822. title: Text(
  823. 'Bluetooth adapter is ${state.toString().substring(15)}',
  824. style: Theme.of(context).primaryTextTheme.subhead,
  825. ),
  826. trailing: Icon(
  827. Icons.error,
  828. color: Theme.of(context).primaryTextTheme.subhead.color,
  829. ),
  830. ),
  831. );
  832. }
  833. }