1
0
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:
Brett Morgan
2023-01-24 11:31:12 +11:00
committed by GitHub
parent 3bc6ad8110
commit 70f3daa9f7
109 changed files with 98007 additions and 0 deletions

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

View 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();
}
}