import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'package:android_intent/android_intent.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blue/flutter_blue.dart'; import 'package:provider/provider.dart'; import 'package:sport/pages/my/device_info_page.dart'; import 'package:sport/provider/bluetooth.dart'; import 'package:sport/widgets/button_cancel.dart'; import 'package:sport/widgets/button_primary.dart'; import 'package:sport/widgets/image.dart'; import 'package:sport/widgets/misc.dart'; import 'package:url_launcher/url_launcher.dart'; class SearchDeviceDialog extends StatefulWidget { @override State createState() => _SearchDeviceDialog(); } class _SearchDeviceDialog extends State { @override void dispose() { super.dispose(); FlutterBlue.instance.stopScan(); } @override Widget build(BuildContext context) { return Dialog( elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), child: Padding( padding: const EdgeInsets.all(8.0), child: ConstrainedBox( constraints: BoxConstraints(maxHeight: 350, minWidth: double.infinity), child: Column( children: [ Stack( alignment: Alignment.center, children: [ Center( child: Padding( padding: const EdgeInsets.all(6.0), child: Text("请选择鞋子", style: Theme.of(context).textTheme.headline3), ), ), Positioned( right: 0, top: 0, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => Navigator.pop(context), child: Padding( padding: const EdgeInsets.all(6.0), child: Image.asset("lib/assets/img/btn_close_big.png"), ), ), ), ], ), Divider(), Expanded( child: StreamBuilder( stream: FlutterBlue.instance.state, initialData: BluetoothState.unknown, builder: (c, snapshot) { final state = snapshot.data; if (state == BluetoothState.on) { return FindDevicesScreen(); } return BluetoothOffScreen(state: state); }), ), ], ), )), ); } } class BluetoothOffScreen extends StatelessWidget { const BluetoothOffScreen({Key key, this.state}) : super(key: key); final BluetoothState state; @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( height: 60, ), Image.asset("lib/assets/img/pop_image_close.png"), SizedBox( height: 20, ), Text( '搜索失败,请确认蓝牙是否打开', style: Theme.of(context).textTheme.bodyText2.copyWith(color: Color(0xff666666)), ), SizedBox( height: 10, ), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () async { if (Platform.isAndroid) { AndroidIntent intent = AndroidIntent(action: "android.bluetooth.adapter.action.REQUEST_ENABLE"); await intent.launch(); } else if (Platform.isIOS) { launch("App-Prefs:root=Bluetooth "); } }, child: Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( '打开蓝牙设置', style: Theme.of(context).textTheme.bodyText2.copyWith(color: Theme.of(context).accentColor), ), SizedBox( width: 6, ), arrowRight6() ], ), ), ), ], ); } } class FindDevicesScreen extends StatefulWidget { @override State createState() => _FindDevicesScreen(); } class _FindDevicesScreen extends State with SingleTickerProviderStateMixin { AnimationController _animationController; Animation _animation; bool _search = true; StreamSubscription streamSubscription; @override void initState() { _animationController = AnimationController(duration: Duration(seconds: 2), vsync: this); _animation = Tween(begin: .0, end: 1.0).animate(_animationController) ..addStatusListener((status) { if (status == AnimationStatus.completed) { _animationController.repeat(); } }); //开始动画 _animationController.forward(); super.initState(); _search = true; startScan(); streamSubscription = Provider.of(context, listen: false).stateStream.listen((event) async { await Future.delayed(Duration(seconds: 2)); Navigator.pop(context); }); } startScan() async { setState(() { _search = true; }); FlutterBlue.instance.startScan(timeout: Duration(seconds: 10)).whenComplete(() { setState(() { _search = false; }); }); } @override void dispose() { _animationController?.dispose(); super.dispose(); streamSubscription?.cancel(); } @override Widget build(BuildContext context) { return Column( children: [ Expanded( child: SingleChildScrollView( child: Column( children: [ if (_search) Padding( padding: const EdgeInsets.symmetric(vertical: 30), child: Column( children: [ Stack( alignment: Alignment.center, children: [ RotationTransition(turns: _animation, child: Image.asset("lib/assets/img/pop_image_circle.png")), Image.asset("lib/assets/img/pop_image_shoes.png") ], ), SizedBox( height: 12, ), Text("搜索中...") ], ), ), // if (!_search) // StreamBuilder>( // stream: Stream.fromFuture(FlutterBlue.instance.connectedDevices), // initialData: [], // builder: (c, snapshot) => Column( // children: snapshot.data // .map((d) => Column( // children: [ // ListTile( // title: Column( // mainAxisAlignment: MainAxisAlignment.start, // crossAxisAlignment: CrossAxisAlignment.start, // children: [ // Text( // d.name, // overflow: TextOverflow.ellipsis, // style: Theme.of(context).textTheme.headline3, // ), // SizedBox( // height: 4, // ), // Text( // d.id.toString(), // style: Theme.of(context).textTheme.caption, // ), // ], // ), // trailing: StreamBuilder( // stream: d.state, // initialData: BluetoothDeviceState.disconnected, // builder: (c, snapshot) { // return GestureDetector( // onTap: () => Navigator.of(context).push(MaterialPageRoute(builder: (context) => DeviceInfoPage())), // child: Container( // width: 58, // height: 30, // alignment: Alignment.center, // decoration: BoxDecoration( // shape: BoxShape.rectangle, // borderRadius: BorderRadius.all(Radius.circular(100)), // color: Theme.of(context).accentColor), // child: Text( // snapshot.data == BluetoothDeviceState.connected ? "已连接" : "未连接", // style: TextStyle(color: Colors.white, fontSize: 12), // ), // ), // ); // }, // ), // ), // Divider() // ], // )) // .toList(), // ), // ), if (!_search) StreamBuilder>( stream: FlutterBlue.instance.scanResults, initialData: [], builder: (c, snapshot) { var bluetooth = Provider.of(context, listen: false); var list = snapshot.data .where((element) => element.device.name.isNotEmpty) .toList(); list.sort((a, b) => bluetooth.device == a.device ? -1 : 1); return Column( children: list.isEmpty == true ? [ Padding( padding: const EdgeInsets.all(30.0), child: Center( child: Column( children: [ Image.asset("lib/assets/img/pop_image_noequipment.png"), SizedBox( height: 10, ), Text("暂无设备") ], ), )) ] : list .map( (r) => Column( children: [ ScanResultTile( result: r, onTap: () async { await Provider.of(context, listen: false).saveDevice(r.device); setState(() {}); }, ), Divider() ], ), ) .toList(), ); }, ), ], ), ), ), Padding( padding: const EdgeInsets.all(8.0), child: StreamBuilder( stream: FlutterBlue.instance.isScanning, initialData: false, builder: (c, snapshot) { if (snapshot.data) { return CancelButton( height: 35, content: "取消", callback: () { FlutterBlue.instance.stopScan(); setState(() { _search = false; }); }, ); } else { return PrimaryButton(height: 35, content: "重新搜索", callback: () => startScan()); } }, ), ), ], ); } } class DeviceScreen extends StatelessWidget { const DeviceScreen({Key key, this.device}) : super(key: key); final BluetoothDevice device; List _getRandomBytes() { final math = Random(); return [math.nextInt(255), math.nextInt(255), math.nextInt(255), math.nextInt(255)]; } List _buildServiceTiles(List services) { return services .map( (s) => ServiceTile( service: s, characteristicTiles: s.characteristics .map( (c) => CharacteristicTile( characteristic: c, onReadPressed: () => c.read(), onWritePressed: () async { await c.write(_getRandomBytes(), withoutResponse: true); await c.read(); }, onNotificationPressed: () async { await c.setNotifyValue(!c.isNotifying); c.value.listen((value) { print(value.length); }); }, descriptorTiles: c.descriptors .map( (d) => DescriptorTile( descriptor: d, onReadPressed: () => d.read(), onWritePressed: () => d.write(_getRandomBytes()), ), ) .toList(), ), ) .toList(), ), ) .toList(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(device.name), actions: [ StreamBuilder( stream: device.state, initialData: BluetoothDeviceState.connecting, builder: (c, snapshot) { VoidCallback onPressed; String text; switch (snapshot.data) { case BluetoothDeviceState.connected: onPressed = () => device.disconnect(); text = 'DISCONNECT'; break; case BluetoothDeviceState.disconnected: onPressed = () => device.connect(); text = 'CONNECT'; break; default: onPressed = null; text = snapshot.data.toString().substring(21).toUpperCase(); break; } return FlatButton( onPressed: onPressed, child: Text( text, style: Theme.of(context).primaryTextTheme.button.copyWith(color: Colors.white), )); }, ) ], ), body: SingleChildScrollView( child: Column( children: [ StreamBuilder( stream: device.state, initialData: BluetoothDeviceState.connecting, builder: (c, snapshot) => ListTile( leading: (snapshot.data == BluetoothDeviceState.connected) ? Icon(Icons.bluetooth_connected) : Icon(Icons.bluetooth_disabled), title: Text('Device is ${snapshot.data.toString().split('.')[1]}.'), subtitle: Text('${device.id}'), trailing: StreamBuilder( stream: device.isDiscoveringServices, initialData: false, builder: (c, snapshot) => IndexedStack( index: snapshot.data ? 1 : 0, children: [ IconButton( icon: Icon(Icons.refresh), onPressed: () => device.discoverServices(), ), IconButton( icon: SizedBox( child: CircularProgressIndicator( valueColor: AlwaysStoppedAnimation(Colors.grey), ), width: 18.0, height: 18.0, ), onPressed: null, ) ], ), ), ), ), StreamBuilder( stream: device.mtu, initialData: 0, builder: (c, snapshot) => ListTile( title: Text('MTU Size'), subtitle: Text('${snapshot.data} bytes'), trailing: IconButton( icon: Icon(Icons.edit), onPressed: () => device.requestMtu(223), ), ), ), StreamBuilder>( stream: device.services, initialData: [], builder: (c, snapshot) { return Column( children: _buildServiceTiles(snapshot.data), ); }, ), RaisedButton( onPressed: () { Provider.of(context, listen: false).queryDeviceStep(); }, child: Text("同步步数"), ), RaisedButton( onPressed: () { Provider.of(context, listen: false).resetData(); }, child: Text("清零"), ), RaisedButton( onPressed: () { Provider.of(context, listen: false).setupGameMode(true); }, child: Text("游戏模式开"), ), RaisedButton( onPressed: () { Provider.of(context, listen: false).setupGameMode(false); }, child: Text("游戏模式关"), ), ValueListenableBuilder( valueListenable: Provider.of(context, listen: false).actionNotifier, builder: (BuildContext context, int value, Widget child) => Text("当前动作: $value"), ), ValueListenableBuilder( valueListenable: Provider.of(context, listen: false).stepTotalNotifier, builder: (BuildContext context, int value, Widget child) => Text("同步步数: $value"), ), ValueListenableBuilder( valueListenable: Provider.of(context, listen: false).stepNotifier, builder: (BuildContext context, int value, Widget child) => Text("相对步数: $value"), ), ValueListenableBuilder( valueListenable: Provider.of(context, listen: false).byteNotifier, builder: (BuildContext context, List value, Widget child) => Text("接收: $value"), ), ], ), ), ); } } class ScanResultTile extends StatelessWidget { const ScanResultTile({Key key, this.result, this.onTap}) : super(key: key); final ScanResult result; final VoidCallback onTap; Widget _buildTitle(BuildContext context) { if (result.device.name.length > 0) { return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( result.device.name, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.headline3, ), SizedBox( height: 4, ), Text( result.device.id.toString(), style: Theme.of(context).textTheme.caption, ), ], ); } else { return Text(result.device.id.toString()); } } Widget _buildAdvRow(BuildContext context, String title, String value) { return Padding( padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 4.0), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: Theme.of(context).textTheme.caption), SizedBox( width: 12.0, ), Expanded( child: Text( value, style: Theme.of(context).textTheme.caption.apply(color: Colors.black), softWrap: true, ), ), ], ), ); } String getNiceHexArray(List bytes) { return '[${bytes.map((i) => i.toRadixString(16).padLeft(2, '0')).join(', ')}]'.toUpperCase(); } String getNiceManufacturerData(Map> data) { if (data.isEmpty) { return null; } List res = []; data.forEach((id, bytes) { res.add('${id.toRadixString(16).toUpperCase()}: ${getNiceHexArray(bytes)}'); }); return res.join(', '); } String getNiceServiceData(Map> data) { if (data.isEmpty) { return null; } List res = []; data.forEach((id, bytes) { res.add('${id.toUpperCase()}: ${getNiceHexArray(bytes)}'); }); return res.join(', '); } @override Widget build(BuildContext context) { return ListTile( contentPadding: EdgeInsets.symmetric(horizontal: 12), title: _buildTitle(context), // leading: Text(result.rssi.toString()), trailing: StreamBuilder( stream: result.device.state, initialData: BluetoothDeviceState.connecting, builder: (c, snapshot) { if (snapshot.data == BluetoothDeviceState.connected) { return Container( width: 64, height: 30, alignment: Alignment.center, decoration: BoxDecoration(shape: BoxShape.rectangle, borderRadius: BorderRadius.all(Radius.circular(100)), color: Theme.of(context).accentColor), child: Text( snapshot.data == BluetoothDeviceState.connected ? "已连接" : "未连接", style: TextStyle(color: Colors.white, fontSize: 12), ), ); } int status = Provider.of(context, listen: false).device == result.device ? 1 : snapshot.data == BluetoothDeviceState.disconnected ? 0 : snapshot.data == BluetoothDeviceState.connecting ? 1 : snapshot.data == BluetoothDeviceState.connected ? 2 : 3; return GestureDetector( onTap: onTap, child: Container( width: 64, height: 30, alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(100)), border: Border.all( color: Theme.of(context).accentColor, )), child: Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( status == 1 ? "连接中" : status == 0 ? "连接" : "已连接", style: TextStyle( color: Theme.of(context).accentColor, fontSize: 12, ), strutStyle: fixedLine, ), if (status == 1) Padding( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: SizedBox( width: 6, height: 6, child: CircularProgressIndicator( strokeWidth: 2, ), ), ), ], ), ), ), ); }, ), // children: [ // _buildAdvRow(context, 'Complete Local Name', result.advertisementData.localName), // _buildAdvRow(context, 'Tx Power Level', '${result.advertisementData.txPowerLevel ?? 'N/A'}'), // _buildAdvRow(context, 'Manufacturer Data', getNiceManufacturerData(result.advertisementData.manufacturerData) ?? 'N/A'), // _buildAdvRow(context, 'Service UUIDs', // (result.advertisementData.serviceUuids.isNotEmpty) ? result.advertisementData.serviceUuids.join(', ').toUpperCase() : 'N/A'), // _buildAdvRow(context, 'Service Data', getNiceServiceData(result.advertisementData.serviceData) ?? 'N/A'), // ], ); } } class ServiceTile extends StatelessWidget { final BluetoothService service; final List characteristicTiles; const ServiceTile({Key key, this.service, this.characteristicTiles}) : super(key: key); @override Widget build(BuildContext context) { if (characteristicTiles.length > 0) { return ExpansionTile( title: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Service'), Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}', style: Theme.of(context).textTheme.body1.copyWith(color: Theme.of(context).textTheme.caption.color)) ], ), children: characteristicTiles, ); } else { return ListTile( title: Text('Service'), subtitle: Text('0x${service.uuid.toString().toUpperCase().substring(4, 8)}'), ); } } } class CharacteristicTile extends StatelessWidget { final BluetoothCharacteristic characteristic; final List descriptorTiles; final VoidCallback onReadPressed; final VoidCallback onWritePressed; final VoidCallback onNotificationPressed; const CharacteristicTile({Key key, this.characteristic, this.descriptorTiles, this.onReadPressed, this.onWritePressed, this.onNotificationPressed}) : super(key: key); @override Widget build(BuildContext context) { return StreamBuilder>( stream: characteristic.value, initialData: characteristic.lastValue, builder: (c, snapshot) { final value = snapshot.data; return ExpansionTile( title: ListTile( title: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Characteristic'), Text('0x${characteristic.uuid.toString().toUpperCase().substring(4, 8)}', style: Theme.of(context).textTheme.body1.copyWith(color: Theme.of(context).textTheme.caption.color)) ], ), subtitle: Text(value.toString()), contentPadding: EdgeInsets.all(0.0), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: Icon( Icons.file_download, color: Theme.of(context).iconTheme.color.withOpacity(0.5), ), onPressed: onReadPressed, ), IconButton( icon: Icon(Icons.file_upload, color: Theme.of(context).iconTheme.color.withOpacity(0.5)), onPressed: onWritePressed, ), IconButton( icon: Icon(characteristic.isNotifying ? Icons.sync_disabled : Icons.sync, color: Theme.of(context).iconTheme.color.withOpacity(0.5)), onPressed: onNotificationPressed, ) ], ), children: descriptorTiles, ); }, ); } } class DescriptorTile extends StatelessWidget { final BluetoothDescriptor descriptor; final VoidCallback onReadPressed; final VoidCallback onWritePressed; const DescriptorTile({Key key, this.descriptor, this.onReadPressed, this.onWritePressed}) : super(key: key); @override Widget build(BuildContext context) { return ListTile( title: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Descriptor'), Text('0x${descriptor.uuid.toString().toUpperCase().substring(4, 8)}', style: Theme.of(context).textTheme.body1.copyWith(color: Theme.of(context).textTheme.caption.color)) ], ), subtitle: StreamBuilder>( stream: descriptor.value, initialData: descriptor.lastValue, builder: (c, snapshot) => Text(snapshot.data.toString()), ), trailing: Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( icon: Icon( Icons.file_download, color: Theme.of(context).iconTheme.color.withOpacity(0.5), ), onPressed: onReadPressed, ), IconButton( icon: Icon( Icons.file_upload, color: Theme.of(context).iconTheme.color.withOpacity(0.5), ), onPressed: onWritePressed, ) ], ), ); } } class AdapterStateTile extends StatelessWidget { const AdapterStateTile({Key key, @required this.state}) : super(key: key); final BluetoothState state; @override Widget build(BuildContext context) { return Container( color: Colors.redAccent, child: ListTile( title: Text( 'Bluetooth adapter is ${state.toString().substring(15)}', style: Theme.of(context).primaryTextTheme.subhead, ), trailing: Icon( Icons.error, color: Theme.of(context).primaryTextTheme.subhead.color, ), ), ); } }