Android 平台开发 (#1)
* 修复 UUID 创建失败的问题 * 移除 scanning 属性 * 临时提交 * CentralManager 开发 & 示例项目开发 * CentralManager 开发 & 示例项目开发 * android 插件生命周期监听 * 修改 API * 示例程序开发 * 修改字体,添加 API,解决后台问题 * Central#connect API * 蓝牙连接部分开发 * 蓝牙连接部分开发 * 解决一些问题 * 解决一些问题 * Connect API 优化 * 添加 API * example 开发 * API 基本完成 * 消息重命名 * API 修改,Android 实现 * 删除多余代码 * 删除多余文件 * 解决 descriptor 自动生成报错的问题 * 还原 Kotlin 版本,广播处理代码迁移至 dart 端 * Kotlin 版本升至 1.5.20 * 解决特征值通知没有在主线程触发的问题,优化代码 * 引入哈希值,避免对象销毁后继续使用 * 使用下拉刷新代替搜索按钮 * 解决由于热重载和蓝牙关闭产生的问题 * 更新插件信息 * 更新 README 和 CHANGELOG * 更新许可证 * 添加注释 * 添加注释,central 拆分
This commit is contained in:
@ -33,9 +33,8 @@ android {
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "dev.yanshouwang.bluetooth_low_energy_example"
|
||||
minSdkVersion 16
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
@ -1,8 +1,9 @@
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.5.20'
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven { url 'https://mirrors.cloud.tencent.com/nexus/repository/maven-public/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -14,7 +15,8 @@ buildscript {
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven { url 'https://mirrors.cloud.tencent.com/nexus/repository/maven-public/' }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,4 +3,5 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||
#distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
|
||||
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-6.7-all.zip
|
||||
|
BIN
example/fonts/IBMPlexMono-Bold.ttf
Normal file
BIN
example/fonts/IBMPlexMono-Bold.ttf
Normal file
Binary file not shown.
BIN
example/fonts/IBMPlexMono-Italic.ttf
Normal file
BIN
example/fonts/IBMPlexMono-Italic.ttf
Normal file
Binary file not shown.
BIN
example/fonts/IBMPlexMono-Regular.ttf
Normal file
BIN
example/fonts/IBMPlexMono-Regular.ttf
Normal file
Binary file not shown.
@ -1,8 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
|
||||
import 'views.dart';
|
||||
|
||||
void main() {
|
||||
runApp(MyApp());
|
||||
@ -22,14 +20,13 @@ class _MyAppState extends State<MyApp> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Plugin example app'),
|
||||
),
|
||||
body: Center(
|
||||
child: Text('Running on: \n'),
|
||||
),
|
||||
theme: ThemeData(
|
||||
fontFamily: 'IBM Plex Mono',
|
||||
),
|
||||
home: HomeView(),
|
||||
routes: {
|
||||
'gatt': (context) => GattView(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
3
example/lib/views.dart
Normal file
3
example/lib/views.dart
Normal file
@ -0,0 +1,3 @@
|
||||
export 'views/home_view.dart';
|
||||
export 'views/gatt_view.dart';
|
||||
export 'views/flip_view.dart';
|
37
example/lib/views/flip_view.dart
Normal file
37
example/lib/views/flip_view.dart
Normal file
@ -0,0 +1,37 @@
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
383
example/lib/views/gatt_view.dart
Normal file
383
example/lib/views/gatt_view.dart
Normal file
@ -0,0 +1,383 @@
|
||||
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';
|
||||
}
|
||||
}
|
232
example/lib/views/home_view.dart
Normal file
232
example/lib/views/home_view.dart
Normal file
@ -0,0 +1,232 @@
|
||||
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
example/lib/widgets.dart
Normal file
1
example/lib/widgets.dart
Normal file
@ -0,0 +1 @@
|
||||
export 'widgets/flip_card.dart';
|
106
example/lib/widgets/flip_card.dart
Normal file
106
example/lib/widgets/flip_card.dart
Normal file
@ -0,0 +1,106 @@
|
||||
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!],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -51,7 +51,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
convert:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: convert
|
||||
url: "https://pub.flutter-io.cn"
|
||||
@ -83,6 +83,13 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_simple_treeview:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_simple_treeview
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.0-nullsafety.1"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -21,6 +21,8 @@ dependencies:
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.2
|
||||
convert: ^3.0.0
|
||||
flutter_simple_treeview: ^3.0.0-nullsafety.1
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@ -68,3 +70,12 @@ flutter:
|
||||
#
|
||||
# For details regarding fonts from package dependencies,
|
||||
# see https://flutter.dev/custom-fonts/#from-packages
|
||||
|
||||
fonts:
|
||||
- family: IBM Plex Mono
|
||||
fonts:
|
||||
- asset: fonts/IBMPlexMono-Regular.ttf
|
||||
- asset: fonts/IBMPlexMono-Italic.ttf
|
||||
style: italic
|
||||
- asset: fonts/IBMPlexMono-Bold.ttf
|
||||
weight: 700
|
||||
|
Reference in New Issue
Block a user