更新示例程序

This commit is contained in:
闫守旺
2021-07-02 09:33:58 +08:00
parent a26432c5b3
commit ed8e4f8868
8 changed files with 604 additions and 794 deletions

View File

@ -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() { 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 @override
_MyAppState createState() => _MyAppState(); _HomeViewState createState() => _HomeViewState();
} }
class _MyAppState extends State<MyApp> { class _HomeViewState extends State<HomeView> with WidgetsBindingObserver {
final ValueNotifier<bool> discovering;
final ValueNotifier<Map<MAC, Discovery>> discoveries;
late StreamSubscription<bool> stateSubscription;
late StreamSubscription<Discovery> 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<bool>(
future: central.state,
builder: (context, snapshot) => snapshot.hasData
? StreamBuilder<bool>(
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<MAC, Discovery> 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 = <Widget>[
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<GattView> {
final ValueNotifier<ConnectionState> state;
GATT? gatt;
StreamSubscription? connectionLostSubscription;
final ValueNotifier<GattService?> service;
final ValueNotifier<GattCharacteristic?> characteristic;
final TextEditingController writeController;
final ValueNotifier<Map<GattCharacteristic, StreamSubscription>> notifies;
final ValueNotifier<List<String>> logs;
late MAC address;
_GattViewState()
: state = ValueNotifier(ConnectionState.disconnected),
service = ValueNotifier(null),
characteristic = ValueNotifier(null),
writeController = TextEditingController(),
notifies = ValueNotifier({}),
logs = ValueNotifier([]);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -19,14 +280,344 @@ class _MyAppState extends State<MyApp> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( address = ModalRoute.of(context)!.settings.arguments as MAC;
theme: ThemeData( return Scaffold(
fontFamily: 'IBM Plex Mono', appBar: AppBar(
title: Text('$address'),
actions: [
connectionView,
],
), ),
home: HomeView(), body: bodyView,
routes: { );
'gatt': (context) => GattView(), }
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<GattService>(
value: service,
child: Text(
service.uuid.name,
softWrap: false,
),
))
.toList();
final serviceView = DropdownButton<GattService>(
isExpanded: true,
hint: Text('选择服务'),
value: serviceValue,
items: services,
onChanged: (value) {
service.value = value;
characteristic.value = null;
},
);
final views = <Widget>[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<GattCharacteristic, StreamSubscription>
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<GattCharacteristic>(
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<String> 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,
} }

View File

@ -1,3 +0,0 @@
export 'views/home_view.dart';
export 'views/gatt_view.dart';
export 'views/flip_view.dart';

View File

@ -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,
),
),
),
);
}
}

View File

@ -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<GattView> {
final ValueNotifier<ConnectionState> state;
GATT? gatt;
StreamSubscription? connectionLostSubscription;
final ValueNotifier<GattService?> service;
final ValueNotifier<GattCharacteristic?> characteristic;
final TextEditingController writeController;
final ValueNotifier<Map<GattCharacteristic, StreamSubscription>> notifies;
final ValueNotifier<List<String>> 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<GattService>(
value: service,
child: Text(
service.uuid.name,
softWrap: false,
),
))
.toList();
final serviceView = DropdownButton<GattService>(
isExpanded: true,
hint: Text('选择服务'),
value: serviceValue,
items: services,
onChanged: (value) {
service.value = value;
characteristic.value = null;
},
);
final views = <Widget>[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<GattCharacteristic, StreamSubscription>
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<GattCharacteristic>(
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<String> 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';
}
}

View File

@ -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<HomeView> with WidgetsBindingObserver {
final ValueNotifier<bool> discovering;
final ValueNotifier<Map<MAC, Discovery>> discoveries;
late StreamSubscription<bool> stateSubscription;
late StreamSubscription<Discovery> 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<bool>(
future: central.state,
builder: (context, snapshot) => snapshot.hasData
? StreamBuilder<bool>(
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<MAC, Discovery> 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 = <Widget>[
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,
),
),
),
);
}
}

View File

@ -1 +0,0 @@
export 'widgets/flip_card.dart';

View File

@ -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<FlipCard> with TickerProviderStateMixin {
final ValueNotifier<double> angle;
late AnimationController animationController;
late Animation<double> frontAnimation;
late Animation<double> 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>[widget.front!, widget.back!],
),
);
},
),
),
);
}
}

View File

@ -5,23 +5,4 @@
// gestures. You can also use WidgetTester to find child widgets in the widget // 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. // tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart'; void main() {}
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,
);
});
}