From ed8e4f88688ad0042a4977d34aa4580192327dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=97=AB=E5=AE=88=E6=97=BA?= Date: Fri, 2 Jul 2021 09:33:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=A4=BA=E4=BE=8B=E7=A8=8B?= =?UTF-8?q?=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- example/lib/main.dart | 615 ++++++++++++++++++++++++++++- example/lib/views.dart | 3 - example/lib/views/flip_view.dart | 37 -- example/lib/views/gatt_view.dart | 383 ------------------ example/lib/views/home_view.dart | 232 ----------- example/lib/widgets.dart | 1 - example/lib/widgets/flip_card.dart | 106 ----- example/test/widget_test.dart | 21 +- 8 files changed, 604 insertions(+), 794 deletions(-) delete mode 100644 example/lib/views.dart delete mode 100644 example/lib/views/flip_view.dart delete mode 100644 example/lib/views/gatt_view.dart delete mode 100644 example/lib/views/home_view.dart delete mode 100644 example/lib/widgets.dart delete mode 100644 example/lib/widgets/flip_card.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index d6e86bc..9101f6f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,17 +1,278 @@ -import 'package:flutter/material.dart'; +import 'dart:async'; +import 'dart:convert'; -import 'views.dart'; +import 'package:bluetooth_low_energy/bluetooth_low_energy.dart'; +import 'package:convert/convert.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; void main() { - runApp(MyApp()); + final app = MaterialApp( + theme: ThemeData( + fontFamily: 'IBM Plex Mono', + ), + home: HomeView(), + routes: { + 'gatt': (context) => GattView(), + }, + ); + runApp(app); } -class MyApp extends StatefulWidget { +class HomeView extends StatefulWidget { + const HomeView({Key? key}) : super(key: key); + @override - _MyAppState createState() => _MyAppState(); + _HomeViewState createState() => _HomeViewState(); } -class _MyAppState extends State { +class _HomeViewState extends State with WidgetsBindingObserver { + final ValueNotifier discovering; + final ValueNotifier> discoveries; + late StreamSubscription stateSubscription; + late StreamSubscription discoverySubscription; + + _HomeViewState() + : discovering = ValueNotifier(false), + discoveries = ValueNotifier({}); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance!.addObserver(this); + stateSubscription = central.stateChanged.listen((state) { + if (state) { + startDiscovery(); + } else { + discoveries.value = {}; + discovering.value = false; + } + }); + discoverySubscription = central.discovered.listen( + (discovery) { + discoveries.value[discovery.address] = discovery; + discoveries.value = {...discoveries.value}; + }, + ); + startDiscovery(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + } + + @override + void dispose() { + stopDiscovery(); + stateSubscription.cancel(); + discoverySubscription.cancel(); + discoveries.dispose(); + discovering.dispose(); + WidgetsBinding.instance!.removeObserver(this); + print('dispose'); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Home'), + ), + body: bodyView, + ); + } + + void startDiscovery() async { + final state = await central.state; + if (!state) return; + await central.startDiscovery(); + discovering.value = true; + } + + void stopDiscovery() async { + final state = await central.state; + if (!state) return; + await central.stopDiscovery(); + discovering.value = false; + } + + void showAdvertisements(Discovery discovery) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + elevation: 0.0, + builder: (context) => buildAdvertisementsView(discovery), + ); + } + + void showGattView(Discovery discovery) async { + stopDiscovery(); + await Navigator.of(context).pushNamed( + 'gatt', + arguments: discovery.address, + ); + startDiscovery(); + } +} + +extension on _HomeViewState { + Widget get bodyView { + return FutureBuilder( + future: central.state, + builder: (context, snapshot) => snapshot.hasData + ? StreamBuilder( + stream: central.stateChanged, + initialData: snapshot.data, + builder: (context, snapshot) => + snapshot.data! ? discoveriesView : closedView, + ) + : closedView, + ); + } + + Widget get closedView { + return Center( + child: Text('蓝牙未开启'), + ); + } + + Widget get discoveriesView { + return RefreshIndicator( + onRefresh: () async => discoveries.value = {}, + child: ValueListenableBuilder( + valueListenable: discoveries, + builder: (context, Map discoveries, child) { + return ListView.builder( + padding: EdgeInsets.all(6.0), + itemCount: discoveries.length, + itemBuilder: (context, i) { + final discovery = discoveries.values.elementAt(i); + return Card( + color: Colors.amber, + clipBehavior: Clip.antiAlias, + shape: BeveledRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(12.0), + bottomLeft: Radius.circular(12.0)), + ), + margin: EdgeInsets.all(6.0), + key: Key(discovery.address.name), + child: InkWell( + splashColor: Colors.purple, + onTap: () => showGattView(discovery), + onLongPress: () => showAdvertisements(discovery), + child: Container( + height: 100.0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(discovery.name ?? 'NaN'), + Text(discovery.address.name), + ], + ), + Text(discovery.rssi.toString()), + ], + ), + ), + ), + ); + }, + ); + }, + ), + ); + } + + Widget buildAdvertisementsView(Discovery discovery) { + final widgets = [ + Row( + children: [ + Text('Type'), + Expanded( + child: Center( + child: Text('Value'), + ), + ), + ], + ), + Divider(), + ]; + for (final entry in discovery.advertisements.entries) { + final key = entry.key.toRadixString(16).padLeft(2, '0'); + final value = hex.encode(entry.value); + final widget = Row( + children: [ + Text('0x$key'), + Container(width: 12.0), + Expanded( + child: Text( + '$value', + softWrap: true, + ), + ), + ], + ); + widgets.add(widget); + if (entry.key != discovery.advertisements.entries.last.key) { + final divider = Divider(); + widgets.add(divider); + } + } + return Container( + margin: const EdgeInsets.all(12.0), + child: Material( + elevation: 1.0, + shape: BeveledRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(12.0), + bottomRight: Radius.circular(12.0), + ), + ), + clipBehavior: Clip.antiAlias, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: widgets, + ), + ), + ), + ); + } +} + +class GattView extends StatefulWidget { + const GattView({Key? key}) : super(key: key); + + @override + _GattViewState createState() => _GattViewState(); +} + +class _GattViewState extends State { + final ValueNotifier state; + GATT? gatt; + StreamSubscription? connectionLostSubscription; + final ValueNotifier service; + final ValueNotifier characteristic; + final TextEditingController writeController; + final ValueNotifier> notifies; + final ValueNotifier> logs; + + late MAC address; + + _GattViewState() + : state = ValueNotifier(ConnectionState.disconnected), + service = ValueNotifier(null), + characteristic = ValueNotifier(null), + writeController = TextEditingController(), + notifies = ValueNotifier({}), + logs = ValueNotifier([]); + @override void initState() { super.initState(); @@ -19,14 +280,344 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { - return MaterialApp( - theme: ThemeData( - fontFamily: 'IBM Plex Mono', + address = ModalRoute.of(context)!.settings.arguments as MAC; + return Scaffold( + appBar: AppBar( + title: Text('$address'), + actions: [ + connectionView, + ], ), - home: HomeView(), - routes: { - 'gatt': (context) => GattView(), + body: bodyView, + ); + } + + void Function()? disposeListener; + + @override + void dispose() { + super.dispose(); + + if (state.value != ConnectionState.disconnected) { + disposeListener ??= () => disposeGATT(); + state.addListener(disposeListener!); + if (state.value == ConnectionState.connected) { + disconnect(); + } + } else { + state.dispose(); + service.dispose(); + characteristic.dispose(); + notifies.dispose(); + logs.dispose(); + } + } + + void disposeGATT() { + switch (state.value) { + case ConnectionState.connected: + disconnect(); + break; + case ConnectionState.disconnected: + state.removeListener(disposeListener!); + state.dispose(); + service.dispose(); + characteristic.dispose(); + notifies.dispose(); + logs.dispose(); + break; + default: + break; + } + } + + void connect() async { + try { + state.value = ConnectionState.connecting; + gatt = await central.connect(address); + state.value = ConnectionState.connected; + connectionLostSubscription = gatt!.connectionLost.listen( + (errorCode) async { + for (var subscription in notifies.value.values) { + await subscription.cancel(); + } + await connectionLostSubscription!.cancel(); + gatt = null; + connectionLostSubscription = null; + service.value = null; + characteristic.value = null; + notifies.value.clear(); + logs.value.clear(); + state.value = ConnectionState.disconnected; + }, + ); + } on PlatformException { + state.value = ConnectionState.disconnected; + } + } + + void disconnect() async { + try { + state.value = ConnectionState.disconnecting; + await gatt!.disconnect(); + for (var subscription in notifies.value.values) { + await subscription.cancel(); + } + await connectionLostSubscription!.cancel(); + gatt = null; + connectionLostSubscription = null; + service.value = null; + characteristic.value = null; + notifies.value.clear(); + logs.value.clear(); + state.value = ConnectionState.disconnected; + } on PlatformException { + state.value = ConnectionState.connected; + } + } +} + +extension on _GattViewState { + Widget get connectionView { + return ValueListenableBuilder( + valueListenable: state, + builder: (context, ConnectionState stateValue, child) { + void Function()? onPressed; + var data = ''; + switch (stateValue) { + case ConnectionState.disconnected: + onPressed = connect; + data = '连接'; + break; + case ConnectionState.connected: + onPressed = disconnect; + data = '断开'; + break; + default: + break; + } + return TextButton( + onPressed: onPressed, + child: Text( + data, + style: TextStyle( + color: Colors.white, + ), + ), + ); }, ); } + + Widget get bodyView { + return ValueListenableBuilder( + valueListenable: state, + builder: (context, ConnectionState stateValue, child) { + switch (stateValue) { + case ConnectionState.disconnected: + return disconnectedView; + case ConnectionState.connecting: + return connectingView; + case ConnectionState.connected: + return connectedView; + case ConnectionState.disconnecting: + return disconnectingView; + default: + throw UnimplementedError(); + } + }, + ); + } + + Widget get disconnectedView { + return Center( + child: Text('未连接'), + ); + } + + Widget get connectingView { + return Center( + child: Text('正在建立连接'), + ); + } + + Widget get connectedView { + return ValueListenableBuilder( + valueListenable: service, + builder: (context, GattService? serviceValue, child) { + final services = gatt!.services.values + .map((service) => DropdownMenuItem( + value: service, + child: Text( + service.uuid.name, + softWrap: false, + ), + )) + .toList(); + final serviceView = DropdownButton( + isExpanded: true, + hint: Text('选择服务'), + value: serviceValue, + items: services, + onChanged: (value) { + service.value = value; + characteristic.value = null; + }, + ); + final views = [serviceView]; + if (serviceValue != null) { + final characteristics = serviceValue.characteristics.values + .map((characteristic) => DropdownMenuItem( + value: characteristic, + child: Text( + characteristic.uuid.name, + softWrap: false, + ), + )) + .toList(); + final characteristicView = ValueListenableBuilder( + valueListenable: characteristic, + builder: (context, GattCharacteristic? characteristicValue, child) { + final canWrite = characteristicValue != null && + (characteristicValue.canWrite || + characteristicValue.canWriteWithoutResponse); + final canRead = + characteristicValue != null && characteristicValue.canRead; + final canNotify = + characteristicValue != null && characteristicValue.canNotify; + final readAndNotifyView = Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + onPressed: canRead + ? () async { + final value = await characteristicValue!.read(); + final time = DateTime.now().display; + final log = '[$time][READ] ${hex.encode(value)}'; + logs.value = [...logs.value, log]; + } + : null, + icon: Icon(Icons.archive), + ), + ValueListenableBuilder( + valueListenable: notifies, + builder: (context, + Map + notifiesValue, + child) { + final notifying = + notifiesValue.containsKey(characteristicValue); + return IconButton( + onPressed: canNotify + ? () async { + if (notifying) { + await characteristicValue!.notify(false); + await notifiesValue + .remove(characteristicValue)! + .cancel(); + } else { + await characteristicValue!.notify(true); + notifiesValue[characteristicValue] = + characteristicValue.valueChanged + .listen((value) { + final time = DateTime.now().display; + final log = + '[$time][NOTIFY] ${hex.encode(value)}'; + logs.value = [...logs.value, log]; + }); + } + notifies.value = {...notifiesValue}; + } + : null, + icon: Icon( + Icons.notifications, + color: notifying ? Colors.blue : null, + ), + ); + }), + ], + ); + final controllerView = TextField( + controller: writeController, + decoration: InputDecoration( + suffixIcon: IconButton( + onPressed: canWrite + ? () { + final value = utf8.encode(writeController.text); + final withoutResponse = + !characteristicValue!.canWrite; + characteristicValue.write(value, + withoutResponse: withoutResponse); + } + : null, + icon: Icon(Icons.send), + ), + ), + ); + return Column( + children: [ + DropdownButton( + isExpanded: true, + hint: Text('选择特征值'), + value: characteristicValue, + items: characteristics, + onChanged: (value) => characteristic.value = value, + ), + readAndNotifyView, + controllerView, + ], + ); + }, + ); + views.add(characteristicView); + } + final loggerView = Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: ValueListenableBuilder( + valueListenable: logs, + builder: (context, List logsValue, child) { + return ListView.builder( + itemCount: logsValue.length, + itemBuilder: (context, i) { + final log = logsValue[i]; + return Text(log); + }, + ); + }), + ), + ); + views.add(loggerView); + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: views, + ), + ); + }, + ); + } + + Widget get disconnectingView { + return Center( + child: Text('正在断开连接'), + ); + } +} + +extension on DateTime { + String get display { + final hh = hour.toString().padLeft(2, '0'); + final mm = minute.toString().padLeft(2, '0'); + final ss = second.toString().padLeft(2, '0'); + return '$hh:$mm:$ss'; + } +} + +enum ConnectionState { + disconnected, + connecting, + connected, + disconnecting, } diff --git a/example/lib/views.dart b/example/lib/views.dart deleted file mode 100644 index 3609ff9..0000000 --- a/example/lib/views.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'views/home_view.dart'; -export 'views/gatt_view.dart'; -export 'views/flip_view.dart'; diff --git a/example/lib/views/flip_view.dart b/example/lib/views/flip_view.dart deleted file mode 100644 index 743bd95..0000000 --- a/example/lib/views/flip_view.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:bluetooth_low_energy_example/widgets.dart'; -import 'package:flutter/material.dart'; - -class FlipView extends StatelessWidget { - final front = Container( - height: 300, - width: 300, - color: Colors.orange, - child: Center( - child: Text('正面'), - ), - ); - final back = Container( - height: 300, - width: 300, - color: Colors.blue, - child: Center( - child: Text( - '反面', - style: TextStyle(color: Colors.white), - ), - ), - ); - @override - Widget build(BuildContext context) { - return Scaffold( - body: Container( - child: Center( - child: FlipCard( - front: front, - back: back, - ), - ), - ), - ); - } -} diff --git a/example/lib/views/gatt_view.dart b/example/lib/views/gatt_view.dart deleted file mode 100644 index 4d9b79b..0000000 --- a/example/lib/views/gatt_view.dart +++ /dev/null @@ -1,383 +0,0 @@ -import 'dart:async'; -import 'dart:convert'; - -import 'package:bluetooth_low_energy/bluetooth_low_energy.dart'; -import 'package:convert/convert.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -class GattView extends StatefulWidget { - const GattView({Key? key}) : super(key: key); - - @override - _GattViewState createState() => _GattViewState(); -} - -class _GattViewState extends State { - final ValueNotifier state; - GATT? gatt; - StreamSubscription? connectionLostSubscription; - final ValueNotifier service; - final ValueNotifier characteristic; - final TextEditingController writeController; - final ValueNotifier> notifies; - final ValueNotifier> logs; - - late MAC address; - - _GattViewState() - : state = ValueNotifier(ConnectionState.disconnected), - service = ValueNotifier(null), - characteristic = ValueNotifier(null), - writeController = TextEditingController(), - notifies = ValueNotifier({}), - logs = ValueNotifier([]); - - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - address = ModalRoute.of(context)!.settings.arguments as MAC; - return Scaffold( - appBar: AppBar( - title: Text('$address'), - actions: [ - connectionView, - ], - ), - body: bodyView, - ); - } - - void Function()? disposeListener; - - @override - void dispose() { - super.dispose(); - - if (state.value != ConnectionState.disconnected) { - disposeListener ??= () => disposeGATT(); - state.addListener(disposeListener!); - if (state.value == ConnectionState.connected) { - disconnect(); - } - } else { - state.dispose(); - service.dispose(); - characteristic.dispose(); - notifies.dispose(); - logs.dispose(); - } - } - - void disposeGATT() { - switch (state.value) { - case ConnectionState.connected: - disconnect(); - break; - case ConnectionState.disconnected: - state.removeListener(disposeListener!); - state.dispose(); - service.dispose(); - characteristic.dispose(); - notifies.dispose(); - logs.dispose(); - break; - default: - break; - } - } - - void connect() async { - try { - state.value = ConnectionState.connecting; - gatt = await central.connect(address); - state.value = ConnectionState.connected; - connectionLostSubscription = gatt!.connectionLost.listen( - (errorCode) async { - for (var subscription in notifies.value.values) { - await subscription.cancel(); - } - await connectionLostSubscription!.cancel(); - gatt = null; - connectionLostSubscription = null; - service.value = null; - characteristic.value = null; - notifies.value.clear(); - logs.value.clear(); - state.value = ConnectionState.disconnected; - }, - ); - } on PlatformException { - state.value = ConnectionState.disconnected; - } - } - - void disconnect() async { - try { - state.value = ConnectionState.disconnecting; - await gatt!.disconnect(); - for (var subscription in notifies.value.values) { - await subscription.cancel(); - } - await connectionLostSubscription!.cancel(); - gatt = null; - connectionLostSubscription = null; - service.value = null; - characteristic.value = null; - notifies.value.clear(); - logs.value.clear(); - state.value = ConnectionState.disconnected; - } on PlatformException { - state.value = ConnectionState.connected; - } - } -} - -enum ConnectionState { - disconnected, - connecting, - connected, - disconnecting, -} - -extension on _GattViewState { - Widget get connectionView { - return ValueListenableBuilder( - valueListenable: state, - builder: (context, ConnectionState stateValue, child) { - void Function()? onPressed; - var data = ''; - switch (stateValue) { - case ConnectionState.disconnected: - onPressed = connect; - data = '连接'; - break; - case ConnectionState.connected: - onPressed = disconnect; - data = '断开'; - break; - default: - break; - } - return TextButton( - onPressed: onPressed, - child: Text( - data, - style: TextStyle( - color: Colors.white, - ), - ), - ); - }, - ); - } - - Widget get bodyView { - return ValueListenableBuilder( - valueListenable: state, - builder: (context, ConnectionState stateValue, child) { - switch (stateValue) { - case ConnectionState.disconnected: - return disconnectedView; - case ConnectionState.connecting: - return connectingView; - case ConnectionState.connected: - return connectedView; - case ConnectionState.disconnecting: - return disconnectingView; - default: - throw UnimplementedError(); - } - }, - ); - } - - Widget get disconnectedView { - return Center( - child: Text('未连接'), - ); - } - - Widget get connectingView { - return Center( - child: Text('正在建立连接'), - ); - } - - Widget get connectedView { - return ValueListenableBuilder( - valueListenable: service, - builder: (context, GattService? serviceValue, child) { - final services = gatt!.services.values - .map((service) => DropdownMenuItem( - value: service, - child: Text( - service.uuid.name, - softWrap: false, - ), - )) - .toList(); - final serviceView = DropdownButton( - isExpanded: true, - hint: Text('选择服务'), - value: serviceValue, - items: services, - onChanged: (value) { - service.value = value; - characteristic.value = null; - }, - ); - final views = [serviceView]; - if (serviceValue != null) { - final characteristics = serviceValue.characteristics.values - .map((characteristic) => DropdownMenuItem( - value: characteristic, - child: Text( - characteristic.uuid.name, - softWrap: false, - ), - )) - .toList(); - final characteristicView = ValueListenableBuilder( - valueListenable: characteristic, - builder: (context, GattCharacteristic? characteristicValue, child) { - final canWrite = characteristicValue != null && - (characteristicValue.canWrite || - characteristicValue.canWriteWithoutResponse); - final canRead = - characteristicValue != null && characteristicValue.canRead; - final canNotify = - characteristicValue != null && characteristicValue.canNotify; - final readAndNotifyView = Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - IconButton( - onPressed: canRead - ? () async { - final value = await characteristicValue!.read(); - final time = DateTime.now().display; - final log = '[$time][READ] ${hex.encode(value)}'; - logs.value = [...logs.value, log]; - } - : null, - icon: Icon(Icons.archive), - ), - ValueListenableBuilder( - valueListenable: notifies, - builder: (context, - Map - notifiesValue, - child) { - final notifying = - notifiesValue.containsKey(characteristicValue); - return IconButton( - onPressed: canNotify - ? () async { - if (notifying) { - await characteristicValue!.notify(false); - await notifiesValue - .remove(characteristicValue)! - .cancel(); - } else { - await characteristicValue!.notify(true); - notifiesValue[characteristicValue] = - characteristicValue.valueChanged - .listen((value) { - final time = DateTime.now().display; - final log = - '[$time][NOTIFY] ${hex.encode(value)}'; - logs.value = [...logs.value, log]; - }); - } - notifies.value = {...notifiesValue}; - } - : null, - icon: Icon( - Icons.notifications, - color: notifying ? Colors.blue : null, - ), - ); - }), - ], - ); - final controllerView = TextField( - controller: writeController, - decoration: InputDecoration( - suffixIcon: IconButton( - onPressed: canWrite - ? () { - final value = utf8.encode(writeController.text); - final withoutResponse = - !characteristicValue!.canWrite; - characteristicValue.write(value, - withoutResponse: withoutResponse); - } - : null, - icon: Icon(Icons.send), - ), - ), - ); - return Column( - children: [ - DropdownButton( - isExpanded: true, - hint: Text('选择特征值'), - value: characteristicValue, - items: characteristics, - onChanged: (value) => characteristic.value = value, - ), - readAndNotifyView, - controllerView, - ], - ); - }, - ); - views.add(characteristicView); - } - final loggerView = Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: ValueListenableBuilder( - valueListenable: logs, - builder: (context, List logsValue, child) { - return ListView.builder( - itemCount: logsValue.length, - itemBuilder: (context, i) { - final log = logsValue[i]; - return Text(log); - }, - ); - }), - ), - ); - views.add(loggerView); - return Container( - padding: EdgeInsets.symmetric(horizontal: 12.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: views, - ), - ); - }, - ); - } - - Widget get disconnectingView { - return Center( - child: Text('正在断开连接'), - ); - } -} - -extension on DateTime { - String get display { - final hh = hour.toString().padLeft(2, '0'); - final mm = minute.toString().padLeft(2, '0'); - final ss = second.toString().padLeft(2, '0'); - return '$hh:$mm:$ss'; - } -} diff --git a/example/lib/views/home_view.dart b/example/lib/views/home_view.dart deleted file mode 100644 index 06160e5..0000000 --- a/example/lib/views/home_view.dart +++ /dev/null @@ -1,232 +0,0 @@ -import 'dart:async'; - -import 'package:bluetooth_low_energy/bluetooth_low_energy.dart'; -import 'package:convert/convert.dart'; -import 'package:flutter/material.dart'; - -class HomeView extends StatefulWidget { - const HomeView({Key? key}) : super(key: key); - - @override - _HomeViewState createState() => _HomeViewState(); -} - -class _HomeViewState extends State with WidgetsBindingObserver { - final ValueNotifier discovering; - final ValueNotifier> discoveries; - late StreamSubscription stateSubscription; - late StreamSubscription discoverySubscription; - - _HomeViewState() - : discovering = ValueNotifier(false), - discoveries = ValueNotifier({}); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance!.addObserver(this); - stateSubscription = central.stateChanged.listen((state) { - if (state) { - startDiscovery(); - } else { - discoveries.value = {}; - discovering.value = false; - } - }); - discoverySubscription = central.discovered.listen( - (discovery) { - discoveries.value[discovery.address] = discovery; - discoveries.value = {...discoveries.value}; - }, - ); - startDiscovery(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - super.didChangeAppLifecycleState(state); - } - - @override - void dispose() { - stopDiscovery(); - stateSubscription.cancel(); - discoverySubscription.cancel(); - discoveries.dispose(); - discovering.dispose(); - WidgetsBinding.instance!.removeObserver(this); - print('dispose'); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Home'), - ), - body: bodyView, - ); - } - - void startDiscovery() async { - final state = await central.state; - if (!state) return; - await central.startDiscovery(); - discovering.value = true; - } - - void stopDiscovery() async { - final state = await central.state; - if (!state) return; - await central.stopDiscovery(); - discovering.value = false; - } - - void showAdvertisements(Discovery discovery) { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - elevation: 0.0, - builder: (context) => buildAdvertisementsView(discovery), - ); - } - - void showGattView(Discovery discovery) async { - stopDiscovery(); - await Navigator.of(context).pushNamed( - 'gatt', - arguments: discovery.address, - ); - startDiscovery(); - } -} - -extension on _HomeViewState { - Widget get bodyView { - return FutureBuilder( - future: central.state, - builder: (context, snapshot) => snapshot.hasData - ? StreamBuilder( - stream: central.stateChanged, - initialData: snapshot.data, - builder: (context, snapshot) => - snapshot.data! ? discoveriesView : closedView, - ) - : closedView, - ); - } - - Widget get closedView { - return Center( - child: Text('蓝牙未开启'), - ); - } - - Widget get discoveriesView { - return RefreshIndicator( - onRefresh: () async => discoveries.value = {}, - child: ValueListenableBuilder( - valueListenable: discoveries, - builder: (context, Map discoveries, child) { - return ListView.builder( - padding: EdgeInsets.all(6.0), - itemCount: discoveries.length, - itemBuilder: (context, i) { - final discovery = discoveries.values.elementAt(i); - return Card( - color: Colors.amber, - clipBehavior: Clip.antiAlias, - shape: BeveledRectangleBorder( - borderRadius: BorderRadius.only( - topRight: Radius.circular(12.0), - bottomLeft: Radius.circular(12.0)), - ), - margin: EdgeInsets.all(6.0), - key: Key(discovery.address.name), - child: InkWell( - splashColor: Colors.purple, - onTap: () => showGattView(discovery), - onLongPress: () => showAdvertisements(discovery), - child: Container( - height: 100.0, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(discovery.name ?? 'NaN'), - Text(discovery.address.name), - ], - ), - Text(discovery.rssi.toString()), - ], - ), - ), - ), - ); - }, - ); - }, - ), - ); - } - - Widget buildAdvertisementsView(Discovery discovery) { - final widgets = [ - Row( - children: [ - Text('Type'), - Expanded( - child: Center( - child: Text('Value'), - ), - ), - ], - ), - Divider(), - ]; - for (final entry in discovery.advertisements.entries) { - final key = entry.key.toRadixString(16).padLeft(2, '0'); - final value = hex.encode(entry.value); - final widget = Row( - children: [ - Text('0x$key'), - Container(width: 12.0), - Expanded( - child: Text( - '$value', - softWrap: true, - ), - ), - ], - ); - widgets.add(widget); - if (entry.key != discovery.advertisements.entries.last.key) { - final divider = Divider(); - widgets.add(divider); - } - } - return Container( - margin: const EdgeInsets.all(12.0), - child: Material( - elevation: 1.0, - shape: BeveledRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(12.0), - bottomRight: Radius.circular(12.0), - ), - ), - clipBehavior: Clip.antiAlias, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: widgets, - ), - ), - ), - ); - } -} diff --git a/example/lib/widgets.dart b/example/lib/widgets.dart deleted file mode 100644 index 44a85c5..0000000 --- a/example/lib/widgets.dart +++ /dev/null @@ -1 +0,0 @@ -export 'widgets/flip_card.dart'; diff --git a/example/lib/widgets/flip_card.dart b/example/lib/widgets/flip_card.dart deleted file mode 100644 index 2f015fe..0000000 --- a/example/lib/widgets/flip_card.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -class FlipCard extends StatefulWidget { - final Widget? front; - final Widget? back; - const FlipCard({ - Key? key, - @required this.front, - @required this.back, - }) : assert(front != null), - assert(back != null), - super(key: key); - - @override - _FlipCardState createState() => _FlipCardState(); -} - -class _FlipCardState extends State with TickerProviderStateMixin { - final ValueNotifier angle; - late AnimationController animationController; - late Animation frontAnimation; - late Animation backAnimation; - - _FlipCardState() : angle = ValueNotifier(0.0); - - bool isFront = true; - bool hasHalf = false; - @override - void initState() { - super.initState(); - animationController = AnimationController( - vsync: this, - duration: Duration(milliseconds: 1000), - ); - animationController.addListener(() { - if (animationController.value > 0.5) { - if (hasHalf == false) { - isFront = !isFront; - } - hasHalf = true; - } - setState(() {}); - }); - animationController.addStatusListener((status) { - if (status == AnimationStatus.completed) { - hasHalf = false; - } - }); - frontAnimation = Tween(begin: 0.0, end: 0.5).animate( - CurvedAnimation( - parent: animationController, - curve: Interval(0.0, 0.5, curve: Curves.easeIn), - ), - ); - backAnimation = Tween(begin: 1.5, end: 2.0).animate(CurvedAnimation( - parent: animationController, - curve: Interval(0.5, 1.0, curve: Curves.easeOut))); - } - - void animate() { - animationController.stop(); - animationController.value = 0; - animationController.forward(); - } - - @override - void dispose() { - super.dispose(); - animationController.dispose(); - } - - @override - Widget build(BuildContext context) { - if (animationController.status == AnimationStatus.forward) { - if (hasHalf == true) { - angle.value = backAnimation.value; - } else { - angle.value = frontAnimation.value; - } - } - return GestureDetector( - onHorizontalDragUpdate: (details) { - print(details.delta); - angle.value += 0.01; - }, - child: Container( - child: ValueListenableBuilder( - valueListenable: angle, - builder: (BuildContext context, double angle, Widget? child) { - return Transform( - transform: Matrix4.identity() - ..setEntry(3, 2, 0.001) - ..rotateY(angle), - alignment: Alignment.center, - child: IndexedStack( - index: isFront ? 0 : 1, - children: [widget.front!, widget.back!], - ), - ); - }, - ), - ), - ); - } -} diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index e789314..570e0e4 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -5,23 +5,4 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:bluetooth_low_energy_example/main.dart'; - -void main() { - testWidgets('Verify Platform version', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => widget is Text && - widget.data!.startsWith('Running on:'), - ), - findsOneWidget, - ); - }); -} +void main() {}