mirror of
https://github.com/flutter/samples.git
synced 2025-11-10 14:58:34 +00:00
Add experimental/pedometer (#1587)
* Add `experimental/pedometer` * Fixup for linter warnings * Update CI config
This commit is contained in:
217
experimental/pedometer/example/lib/main.dart
Normal file
217
experimental/pedometer/example/lib/main.dart
Normal file
@@ -0,0 +1,217 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
|
||||
import 'steps_repo.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
home: const Home(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RoundClipper extends CustomClipper<Path> {
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
final diameter = size.shortestSide * 1.5;
|
||||
final x = -(diameter - size.width) / 2;
|
||||
final y = size.height - diameter;
|
||||
final rect = Offset(x, y) & Size(diameter, diameter);
|
||||
return Path()..addOval(rect);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(CustomClipper<Path> oldClipper) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class Home extends StatefulWidget {
|
||||
const Home({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<Home> createState() => _HomeState();
|
||||
}
|
||||
|
||||
class _HomeState extends State<Home> {
|
||||
var hourlySteps = <Steps>[];
|
||||
DateTime? lastUpdated;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
runPedometer();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void runPedometer() async {
|
||||
final now = DateTime.now();
|
||||
hourlySteps = await StepsRepo.instance.getSteps();
|
||||
lastUpdated = now;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
final barGroups = hourlySteps
|
||||
.map(
|
||||
(e) => BarChartGroupData(
|
||||
x: int.parse(e.startHour),
|
||||
barRods: [
|
||||
BarChartRodData(
|
||||
color: Colors.blue[900],
|
||||
toY: e.steps.toDouble() / 100,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
ClipPath(
|
||||
clipper: RoundClipper(),
|
||||
child: FractionallySizedBox(
|
||||
heightFactor: 0.55,
|
||||
widthFactor: 1,
|
||||
child: Container(color: Colors.blue[300]),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(80.0),
|
||||
child: Column(
|
||||
children: [
|
||||
lastUpdated != null
|
||||
? Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 50.0),
|
||||
child: Text(
|
||||
DateFormat.yMMMMd('en_US').format(lastUpdated!),
|
||||
style: textTheme.titleLarge!
|
||||
.copyWith(color: Colors.blue[900]),
|
||||
),
|
||||
)
|
||||
: const SizedBox(height: 0),
|
||||
Text(
|
||||
hourlySteps.fold(0, (t, e) => t + e.steps).toString(),
|
||||
style: textTheme.displayMedium!.copyWith(color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
'steps',
|
||||
style: textTheme.titleLarge!.copyWith(color: Colors.white),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: GestureDetector(
|
||||
onTap: runPedometer,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue[900],
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(
|
||||
Icons.refresh,
|
||||
color: Colors.white,
|
||||
size: 50,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 30.0, vertical: 50.0),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.2,
|
||||
child: BarChart(
|
||||
BarChartData(
|
||||
titlesData: FlTitlesData(
|
||||
show: true,
|
||||
// Top titles are null
|
||||
topTitles:
|
||||
AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles:
|
||||
AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: false,
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 30,
|
||||
getTitlesWidget: getBottomTitles,
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: false,
|
||||
),
|
||||
barGroups: barGroups,
|
||||
gridData: FlGridData(show: false),
|
||||
alignment: BarChartAlignment.spaceAround,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Axis labels for bottom of chart
|
||||
Widget getBottomTitles(double value, TitleMeta meta) {
|
||||
String text;
|
||||
switch (value.toInt()) {
|
||||
case 0:
|
||||
text = '12AM';
|
||||
break;
|
||||
case 6:
|
||||
text = '6AM';
|
||||
break;
|
||||
case 12:
|
||||
text = '12PM';
|
||||
break;
|
||||
case 18:
|
||||
text = '6PM';
|
||||
break;
|
||||
default:
|
||||
text = '';
|
||||
}
|
||||
return SideTitleWidget(
|
||||
axisSide: meta.axisSide,
|
||||
space: 4,
|
||||
child: Text(text, style: TextStyle(fontSize: 14, color: Colors.blue[900])),
|
||||
);
|
||||
}
|
||||
166
experimental/pedometer/example/lib/steps_repo.dart
Normal file
166
experimental/pedometer/example/lib/steps_repo.dart
Normal file
@@ -0,0 +1,166 @@
|
||||
// ignore_for_file: depend_on_referenced_packages
|
||||
|
||||
import 'dart:ffi' as ffi;
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:jni/jni.dart' as jni;
|
||||
import 'package:pedometer/pedometer_bindings_generated.dart' as pd;
|
||||
import 'package:pedometer/health_connect.dart' as hc;
|
||||
|
||||
/// Class to hold the information needed for the chart
|
||||
class Steps {
|
||||
String startHour;
|
||||
int steps;
|
||||
Steps(this.startHour, this.steps);
|
||||
}
|
||||
|
||||
abstract class StepsRepo {
|
||||
static const _formatString = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
static StepsRepo? _instance;
|
||||
static StepsRepo get instance =>
|
||||
_instance ??= Platform.isAndroid ? _AndroidStepsRepo() : _IOSStepsRepo();
|
||||
|
||||
Future<List<Steps>> getSteps();
|
||||
}
|
||||
|
||||
class _IOSStepsRepo implements StepsRepo {
|
||||
static const _dylibPath =
|
||||
'/System/Library/Frameworks/CoreMotion.framework/CoreMotion';
|
||||
|
||||
// Bindings for the CMPedometer class
|
||||
final lib = pd.PedometerBindings(ffi.DynamicLibrary.open(_dylibPath));
|
||||
// Bindings for the helper function
|
||||
final helpLib = pd.PedometerBindings(ffi.DynamicLibrary.process());
|
||||
|
||||
late final pd.CMPedometer client;
|
||||
late final pd.NSDateFormatter formatter;
|
||||
late final pd.NSDateFormatter hourFormatter;
|
||||
|
||||
_IOSStepsRepo() {
|
||||
// Contains the Dart API helper functions
|
||||
final dylib = ffi.DynamicLibrary.open("pedometer.framework/pedometer");
|
||||
|
||||
// Initialize the Dart API
|
||||
final initializeApi = dylib.lookupFunction<
|
||||
ffi.IntPtr Function(ffi.Pointer<ffi.Void>),
|
||||
int Function(ffi.Pointer<ffi.Void>)>('Dart_InitializeApiDL');
|
||||
|
||||
final initializeResult = initializeApi(ffi.NativeApi.initializeApiDLData);
|
||||
if (initializeResult != 0) {
|
||||
throw StateError('failed to init API.');
|
||||
}
|
||||
|
||||
// Create a new CMPedometer instance.
|
||||
client = pd.CMPedometer.new1(lib);
|
||||
|
||||
// Setting the formatter for date strings.
|
||||
formatter =
|
||||
pd.NSDateFormatter.castFrom(pd.NSDateFormatter.alloc(lib).init());
|
||||
formatter.dateFormat = pd.NSString(lib, "${StepsRepo._formatString} zzz");
|
||||
hourFormatter =
|
||||
pd.NSDateFormatter.castFrom(pd.NSDateFormatter.alloc(lib).init());
|
||||
hourFormatter.dateFormat = pd.NSString(lib, "HH");
|
||||
}
|
||||
|
||||
pd.NSDate dateConverter(DateTime dartDate) {
|
||||
// Format dart date to string.
|
||||
final formattedDate = DateFormat(StepsRepo._formatString).format(dartDate);
|
||||
// Get current timezone. If eastern african change to AST to follow with NSDate.
|
||||
final tz = dartDate.timeZoneName == "EAT" ? "AST" : dartDate.timeZoneName;
|
||||
|
||||
// Create a new NSString with the formatted date and timezone.
|
||||
final nString = pd.NSString(lib, "$formattedDate $tz");
|
||||
// Convert the NSString to NSDate.
|
||||
return formatter.dateFromString_(nString);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Steps>> getSteps() async {
|
||||
if (!pd.CMPedometer.isStepCountingAvailable(lib)) {
|
||||
debugPrint("Step counting is not available.");
|
||||
return [];
|
||||
}
|
||||
|
||||
final futures = <Future>[];
|
||||
final now = DateTime.now();
|
||||
|
||||
for (var h = 0; h <= now.hour; h++) {
|
||||
// Open up a port to receive data from native side.
|
||||
final receivePort = ReceivePort();
|
||||
final nativePort = receivePort.sendPort.nativePort;
|
||||
final start = dateConverter(DateTime(now.year, now.month, now.day, h));
|
||||
final end = dateConverter(DateTime(now.year, now.month, now.day, h + 1));
|
||||
|
||||
pd.PedometerHelper.startPedometerWithPort_pedometer_start_end_(
|
||||
helpLib,
|
||||
nativePort,
|
||||
client,
|
||||
start,
|
||||
end,
|
||||
);
|
||||
// Handle the data received from native side.
|
||||
futures.add(receivePort.first);
|
||||
}
|
||||
|
||||
final data = await Future.wait(futures);
|
||||
return data.where((e) => e != null).cast<int>().map((address) {
|
||||
final result = ffi.Pointer<pd.ObjCObject>.fromAddress(address);
|
||||
final pedometerData =
|
||||
pd.CMPedometerData.castFromPointer(lib, result, release: true);
|
||||
final stepCount = pedometerData.numberOfSteps?.intValue ?? 0;
|
||||
final startHour =
|
||||
hourFormatter.stringFromDate_(pedometerData.startDate!).toString();
|
||||
return Steps(startHour, stepCount);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
class _AndroidStepsRepo implements StepsRepo {
|
||||
late final hc.Activity activity;
|
||||
late final hc.Context applicationContext;
|
||||
late final hc.HealthConnectClient client;
|
||||
|
||||
_AndroidStepsRepo() {
|
||||
jni.Jni.initDLApi();
|
||||
activity = hc.Activity.fromRef(jni.Jni.getCurrentActivity());
|
||||
applicationContext =
|
||||
hc.Context.fromRef(jni.Jni.getCachedApplicationContext());
|
||||
client = hc.HealthConnectClient.getOrCreate1(applicationContext);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Steps>> getSteps() async {
|
||||
final futures = <Future<hc.AggregationResult>>[];
|
||||
final now = DateTime.now();
|
||||
|
||||
for (var h = 0; h <= now.hour; h++) {
|
||||
final start =
|
||||
DateTime(now.year, now.month, now.day, h).millisecondsSinceEpoch;
|
||||
final end =
|
||||
DateTime(now.year, now.month, now.day, h + 1).millisecondsSinceEpoch;
|
||||
final request = hc.AggregateRequest(
|
||||
hc.Set.of1(
|
||||
hc.AggregateMetric.type(hc.Long.type),
|
||||
hc.StepsRecord.COUNT_TOTAL,
|
||||
),
|
||||
hc.TimeRangeFilter.between(
|
||||
hc.Instant.ofEpochMilli(start),
|
||||
hc.Instant.ofEpochMilli(end),
|
||||
),
|
||||
hc.Set.of(jni.JObject.type),
|
||||
);
|
||||
futures.add(client.aggregate(request));
|
||||
}
|
||||
final data = await Future.wait(futures);
|
||||
return data.asMap().entries.map((entry) {
|
||||
final stepsLong =
|
||||
entry.value.get0(hc.Long.type, hc.StepsRecord.COUNT_TOTAL);
|
||||
final steps = stepsLong.isNull ? 0 : stepsLong.intValue();
|
||||
return Steps(entry.key.toString().padLeft(2, '0'), steps);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user