更新示例程序
This commit is contained in:
@ -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,
|
||||||
}
|
}
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
export 'views/home_view.dart';
|
|
||||||
export 'views/gatt_view.dart';
|
|
||||||
export 'views/flip_view.dart';
|
|
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
export 'widgets/flip_card.dart';
|
|
@ -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!],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
Reference in New Issue
Block a user