Divided the code into functional modules
This commit is contained in:
868
MainPage.xaml.cs
868
MainPage.xaml.cs
File diff suppressed because it is too large
Load Diff
11
Models/HeartRateDataPoint.cs
Normal file
11
Models/HeartRateDataPoint.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace HeartRateMonitorAndroid.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 心率数据点类
|
||||
/// </summary>
|
||||
public class HeartRateDataPoint
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public int HeartRate { get; set; }
|
||||
}
|
||||
}
|
||||
143
Models/HeartRateSessionData.cs
Normal file
143
Models/HeartRateSessionData.cs
Normal file
@ -0,0 +1,143 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace HeartRateMonitorAndroid.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 心率会话数据,包含一次监测会话的所有数据和统计信息
|
||||
/// </summary>
|
||||
public class HeartRateSessionData
|
||||
{
|
||||
private readonly object _heartRateDataLock = new object(); // 线程安全操作的锁对象
|
||||
private List<HeartRateDataPoint> _heartRateData = new List<HeartRateDataPoint>();
|
||||
|
||||
/// <summary>
|
||||
/// 会话开始时间
|
||||
/// </summary>
|
||||
public DateTime SessionStartTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最新心率值
|
||||
/// </summary>
|
||||
public int LatestHeartRate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最小心率值
|
||||
/// </summary>
|
||||
public int MinHeartRate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大心率值
|
||||
/// </summary>
|
||||
public int MaxHeartRate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 平均心率值
|
||||
/// </summary>
|
||||
public double AverageHeartRate { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取心率数据点列表的副本
|
||||
/// </summary>
|
||||
public List<HeartRateDataPoint> HeartRateData
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_heartRateDataLock)
|
||||
{
|
||||
return _heartRateData.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否有新的心率数据
|
||||
/// </summary>
|
||||
public bool HasNewHeartRateData { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化心率会话数据
|
||||
/// </summary>
|
||||
public HeartRateSessionData()
|
||||
{
|
||||
ResetData();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置会话数据
|
||||
/// </summary>
|
||||
public void ResetData()
|
||||
{
|
||||
lock (_heartRateDataLock)
|
||||
{
|
||||
_heartRateData.Clear();
|
||||
LatestHeartRate = 0;
|
||||
MinHeartRate = 0;
|
||||
MaxHeartRate = 0;
|
||||
AverageHeartRate = 0;
|
||||
HasNewHeartRateData = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加新的心率数据点
|
||||
/// </summary>
|
||||
/// <param name="heartRate">心率值</param>
|
||||
public void AddHeartRate(int heartRate)
|
||||
{
|
||||
lock (_heartRateDataLock)
|
||||
{
|
||||
// 添加新的数据点
|
||||
var dataPoint = new HeartRateDataPoint
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
HeartRate = heartRate
|
||||
};
|
||||
|
||||
// 如果是第一个数据点,记录会话开始时间
|
||||
if (_heartRateData.Count == 0)
|
||||
{
|
||||
SessionStartTime = DateTime.Now;
|
||||
}
|
||||
|
||||
_heartRateData.Add(dataPoint);
|
||||
LatestHeartRate = heartRate;
|
||||
|
||||
// 限制数据点数量,保留最新的100个点
|
||||
if (_heartRateData.Count > 100)
|
||||
{
|
||||
_heartRateData.RemoveAt(0);
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
if (_heartRateData.Count > 0)
|
||||
{
|
||||
MinHeartRate = _heartRateData.Min(p => p.HeartRate);
|
||||
MaxHeartRate = _heartRateData.Max(p => p.HeartRate);
|
||||
AverageHeartRate = _heartRateData.Average(p => p.HeartRate);
|
||||
}
|
||||
|
||||
HasNewHeartRateData = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置新数据标记
|
||||
/// </summary>
|
||||
public void ResetNewDataFlag()
|
||||
{
|
||||
HasNewHeartRateData = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前会话的监测时长
|
||||
/// </summary>
|
||||
public TimeSpan GetSessionDuration()
|
||||
{
|
||||
if (_heartRateData.Count == 0)
|
||||
return TimeSpan.Zero;
|
||||
|
||||
return DateTime.Now - SessionStartTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
430
Services/BluetoothService.cs
Normal file
430
Services/BluetoothService.cs
Normal file
@ -0,0 +1,430 @@
|
||||
using System.Diagnostics;
|
||||
using HeartRateMonitorAndroid.Models;
|
||||
using Plugin.BLE;
|
||||
using Plugin.BLE.Abstractions;
|
||||
using Plugin.BLE.Abstractions.Contracts;
|
||||
using Plugin.BLE.Abstractions.EventArgs;
|
||||
|
||||
namespace HeartRateMonitorAndroid.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 蓝牙服务类,用于管理蓝牙设备连接和心率监测
|
||||
/// </summary>
|
||||
public class BluetoothService
|
||||
{
|
||||
private const string TAG = "BluetoothService";
|
||||
|
||||
// 心率服务和特征的UUID常量
|
||||
private static readonly Guid HEART_RATE_SERVICE_UUID = Guid.Parse("0000180D-0000-1000-8000-00805F9B34FB");
|
||||
private static readonly Guid HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID = Guid.Parse("00002A37-0000-1000-8000-00805F9B34FB");
|
||||
|
||||
private readonly IBluetoothLE _ble;
|
||||
private readonly IAdapter _adapter;
|
||||
private bool _isConnecting = false;
|
||||
private IDevice _connectedDevice = null;
|
||||
|
||||
/// <summary>
|
||||
/// 心率数据更新事件
|
||||
/// </summary>
|
||||
public event Action<int> HeartRateUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// 蓝牙状态更新事件
|
||||
/// </summary>
|
||||
public event Action<string> StatusUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// 设备发现事件
|
||||
/// </summary>
|
||||
public event Action<IDevice> DeviceDiscovered;
|
||||
|
||||
/// <summary>
|
||||
/// 当前连接的设备
|
||||
/// </summary>
|
||||
public IDevice ConnectedDevice => _connectedDevice;
|
||||
|
||||
/// <summary>
|
||||
/// 蓝牙是否可用
|
||||
/// </summary>
|
||||
public bool IsBluetoothAvailable => _ble.IsAvailable && _ble.IsOn;
|
||||
|
||||
/// <summary>
|
||||
/// 是否正在扫描
|
||||
/// </summary>
|
||||
public bool IsScanning => _adapter.IsScanning;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化蓝牙服务
|
||||
/// </summary>
|
||||
public BluetoothService()
|
||||
{
|
||||
_ble = CrossBluetoothLE.Current;
|
||||
_adapter = CrossBluetoothLE.Current.Adapter;
|
||||
|
||||
// 注册设备发现事件
|
||||
_adapter.DeviceDiscovered += OnDeviceDiscovered;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查蓝牙状态
|
||||
/// </summary>
|
||||
/// <returns>状态信息</returns>
|
||||
public string CheckBluetoothState()
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 检查 BLE 状态...");
|
||||
|
||||
if (!_ble.IsAvailable)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 设备不支持 BLE");
|
||||
StatusUpdated?.Invoke("设备不支持 BLE");
|
||||
return "设备不支持 BLE";
|
||||
}
|
||||
|
||||
if (!_ble.IsOn)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 蓝牙未开启");
|
||||
StatusUpdated?.Invoke("请开启蓝牙后再试");
|
||||
return "请开启蓝牙后再试";
|
||||
}
|
||||
|
||||
Debug.WriteLine($"{TAG}: BLE 可用且已开启");
|
||||
StatusUpdated?.Invoke("准备就绪,点击开始扫描");
|
||||
return "准备就绪,点击开始扫描";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始扫描设备
|
||||
/// </summary>
|
||||
public async Task StartScanAsync()
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 开始扫描附近设备...");
|
||||
StatusUpdated?.Invoke("正在扫描...");
|
||||
|
||||
try
|
||||
{
|
||||
// 先停止之前的扫描
|
||||
if (_adapter.IsScanning)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 停止之前的扫描");
|
||||
await _adapter.StopScanningForDevicesAsync();
|
||||
// 短暂延迟确保扫描完全停止
|
||||
await Task.Delay(200);
|
||||
}
|
||||
|
||||
// 设置扫描参数
|
||||
_adapter.ScanMode = ScanMode.LowLatency; // 使用低延迟模式提高响应速度
|
||||
|
||||
// 开始全扫描模式
|
||||
Debug.WriteLine($"{TAG}: 开始全扫描模式");
|
||||
await _adapter.StartScanningForDevicesAsync();
|
||||
|
||||
Debug.WriteLine($"{TAG}: 扫描已启动,将自动超时或在发现心率设备时停止");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 扫描出错: {ex.Message}");
|
||||
StatusUpdated?.Invoke($"扫描出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止扫描设备
|
||||
/// </summary>
|
||||
public async Task StopScanAsync()
|
||||
{
|
||||
if (_adapter.IsScanning)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 停止扫描");
|
||||
await _adapter.StopScanningForDevicesAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 停止扫描时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接到心率设备
|
||||
/// </summary>
|
||||
/// <param name="device">要连接的设备</param>
|
||||
public async Task ConnectToDeviceAsync(IDevice device)
|
||||
{
|
||||
// 防止重复连接
|
||||
if (_isConnecting)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 已有连接请求正在进行中,忽略此次连接");
|
||||
return;
|
||||
}
|
||||
|
||||
_isConnecting = true;
|
||||
|
||||
try
|
||||
{
|
||||
// 确保扫描已停止
|
||||
if (_adapter.IsScanning)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 连接前确保扫描已停止");
|
||||
await _adapter.StopScanningForDevicesAsync();
|
||||
await Task.Delay(300); // 确保扫描完全停止
|
||||
}
|
||||
|
||||
StatusUpdated?.Invoke($"正在连接到 {device.Name ?? "未知设备"}...");
|
||||
Debug.WriteLine($"{TAG}: 正在连接到设备: {device.Name ?? "未知设备"}...");
|
||||
|
||||
// 使用CancellationToken添加超时控制
|
||||
var cancelSource = new CancellationTokenSource();
|
||||
cancelSource.CancelAfter(TimeSpan.FromSeconds(15)); // 15秒连接超时
|
||||
|
||||
// 连接到设备
|
||||
try
|
||||
{
|
||||
await _adapter.ConnectToDeviceAsync(device,
|
||||
new ConnectParameters(autoConnect: false, forceBleTransport: true), cancelSource.Token);
|
||||
Debug.WriteLine($"{TAG}: 连接命令已发送,等待连接完成");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 连接操作超时");
|
||||
StatusUpdated?.Invoke("连接超时,请重试");
|
||||
return;
|
||||
}
|
||||
|
||||
if (device.State == DeviceState.Connected)
|
||||
{
|
||||
StatusUpdated?.Invoke($"已连接到 {device.Name ?? "未知设备"}");
|
||||
Debug.WriteLine($"{TAG}: 已连接到设备: {device.Name ?? "未知设备"}");
|
||||
|
||||
// 保存连接的设备引用
|
||||
_connectedDevice = device;
|
||||
|
||||
// 获取心率服务
|
||||
Debug.WriteLine($"{TAG}: 尝试获取心率服务 {HEART_RATE_SERVICE_UUID}");
|
||||
var heartRateService = await device.GetServiceAsync(HEART_RATE_SERVICE_UUID);
|
||||
if (heartRateService == null)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 未找到心率服务");
|
||||
StatusUpdated?.Invoke("未找到心率服务");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.WriteLine($"{TAG}: 已获取心率服务,尝试获取心率特征");
|
||||
// 获取心率特征
|
||||
var heartRateCharacteristic = await heartRateService.GetCharacteristicAsync(HEART_RATE_MEASUREMENT_CHARACTERISTIC_UUID);
|
||||
if (heartRateCharacteristic == null)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 未找到心率特征");
|
||||
StatusUpdated?.Invoke("未找到心率特征");
|
||||
return;
|
||||
}
|
||||
|
||||
// 订阅心率通知
|
||||
heartRateCharacteristic.ValueUpdated += (s, e) =>
|
||||
{
|
||||
// 解析心率数据
|
||||
var data = e.Characteristic.Value;
|
||||
if (data == null || data.Length == 0)
|
||||
return;
|
||||
|
||||
byte flags = data[0];
|
||||
bool isHeartRateValueFormat16Bit = ((flags & 0x01) != 0);
|
||||
int heartRate;
|
||||
|
||||
if (isHeartRateValueFormat16Bit && data.Length >= 3)
|
||||
{
|
||||
heartRate = BitConverter.ToUInt16(data, 1);
|
||||
}
|
||||
else if (data.Length >= 2)
|
||||
{
|
||||
heartRate = data[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
return; // 数据不完整
|
||||
}
|
||||
|
||||
// 触发心率更新事件
|
||||
HeartRateUpdated?.Invoke(heartRate);
|
||||
};
|
||||
|
||||
// 开始接收通知
|
||||
await heartRateCharacteristic.StartUpdatesAsync();
|
||||
StatusUpdated?.Invoke("正在监测心率...");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 连接失败");
|
||||
StatusUpdated?.Invoke("连接失败,请重试");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 连接错误: {ex.Message}");
|
||||
StatusUpdated?.Invoke($"连接错误: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 重置连接状态标志
|
||||
_isConnecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断开设备连接
|
||||
/// </summary>
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
if (_connectedDevice != null && _connectedDevice.State == DeviceState.Connected)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _adapter.DisconnectDeviceAsync(_connectedDevice);
|
||||
_connectedDevice = null;
|
||||
StatusUpdated?.Invoke("设备已断开连接");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 断开连接时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设备发现事件处理
|
||||
/// </summary>
|
||||
private void OnDeviceDiscovered(object sender, DeviceEventArgs args)
|
||||
{
|
||||
var device = args.Device;
|
||||
Debug.WriteLine($"{TAG}: 发现设备: {device.Name ?? "未知设备"} ({device.Id})");
|
||||
|
||||
foreach (var adv in device.AdvertisementRecords)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: Adv Type: {adv.Type}, Data: {BitConverter.ToString(adv.Data)}");
|
||||
}
|
||||
|
||||
// 检查广播数据是否包含心率服务 UUID (0x180D)
|
||||
bool hasHeartRateService = false;
|
||||
|
||||
// 检查16位UUID服务列表
|
||||
var serviceUuids16Bit = device.AdvertisementRecords.FirstOrDefault(r =>
|
||||
r.Type == AdvertisementRecordType.UuidsComplete16Bit ||
|
||||
r.Type == AdvertisementRecordType.UuidsIncomple16Bit);
|
||||
|
||||
if (serviceUuids16Bit != null)
|
||||
{
|
||||
// 心率服务UUID是0x180D,根据日志,数据存储顺序为18-0D
|
||||
string dataString = BitConverter.ToString(serviceUuids16Bit.Data);
|
||||
Debug.WriteLine($"{TAG}: 16位UUID数据: {dataString}");
|
||||
hasHeartRateService = dataString.Contains("18-0D");
|
||||
|
||||
if (hasHeartRateService)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 在16位UUID中找到心率服务(0x180D)");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果16位列表中未找到,则检查128位UUID列表
|
||||
if (!hasHeartRateService)
|
||||
{
|
||||
var serviceUuids128Bit = device.AdvertisementRecords.FirstOrDefault(r =>
|
||||
r.Type == AdvertisementRecordType.UuidsComplete128Bit ||
|
||||
r.Type == AdvertisementRecordType.UuidsIncomplete128Bit);
|
||||
|
||||
if (serviceUuids128Bit != null)
|
||||
{
|
||||
// 心率服务在128位UUID中的格式通常是0000180D-0000-1000-8000-00805F9B34FB
|
||||
// 检查两种可能的排列方式
|
||||
string dataString = BitConverter.ToString(serviceUuids128Bit.Data);
|
||||
Debug.WriteLine($"{TAG}: 128位UUID数据: {dataString}");
|
||||
hasHeartRateService = dataString.Contains("18-0D") || dataString.Contains("0D-18");
|
||||
|
||||
if (hasHeartRateService)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 在128位UUID中找到心率服务(0x180D)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查设备名称,有些心率设备名称中包含相关信息
|
||||
if (!hasHeartRateService && !string.IsNullOrEmpty(device.Name))
|
||||
{
|
||||
string name = device.Name.ToLower();
|
||||
if (name.Contains("heart") || name.Contains("hr") || name.Contains("pulse") ||
|
||||
name.Contains("cardiac") || name.Contains("心率"))
|
||||
{
|
||||
hasHeartRateService = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHeartRateService)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 检测到心率设备: {device.Name ?? "未知设备"}");
|
||||
|
||||
// 立即停止扫描 - 这是重要的,必须先停止扫描再连接
|
||||
if (_adapter.IsScanning)
|
||||
{
|
||||
try
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 停止扫描以准备连接设备");
|
||||
_adapter.StopScanningForDevicesAsync().ContinueWith(t =>
|
||||
{
|
||||
if (t.IsCompleted && !t.IsFaulted)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 扫描已停止,准备连接设备");
|
||||
DeviceDiscovered?.Invoke(device);
|
||||
}
|
||||
else if (t.IsFaulted && t.Exception != null)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 停止扫描失败: {t.Exception.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 停止扫描时出错: {ex.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果没有扫描,直接通知发现了设备
|
||||
DeviceDiscovered?.Invoke(device);
|
||||
}
|
||||
|
||||
// 尝试从广播数据中解析心率值(有些设备可能在广播中包含数据)
|
||||
var manufacturerData = device.AdvertisementRecords.FirstOrDefault(r =>
|
||||
r.Type == AdvertisementRecordType.ManufacturerSpecificData);
|
||||
|
||||
if (manufacturerData != null && manufacturerData.Data.Length > 1)
|
||||
{
|
||||
int heartRate = manufacturerData.Data[1];
|
||||
Debug.WriteLine($"{TAG}: 广播心率值: {heartRate} bpm");
|
||||
HeartRateUpdated?.Invoke(heartRate);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($"{TAG}: 未在广播中找到心率值,将尝试连接设备读取");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放资源
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_adapter != null && _adapter.IsScanning)
|
||||
{
|
||||
_adapter.StopScanningForDevicesAsync().Wait();
|
||||
}
|
||||
|
||||
if (_connectedDevice != null && _connectedDevice.State == DeviceState.Connected)
|
||||
{
|
||||
_adapter.DisconnectDeviceAsync(_connectedDevice).Wait();
|
||||
}
|
||||
|
||||
_adapter.DeviceDiscovered -= OnDeviceDiscovered;
|
||||
}
|
||||
}
|
||||
}
|
||||
36
Services/INotificationService.cs
Normal file
36
Services/INotificationService.cs
Normal file
@ -0,0 +1,36 @@
|
||||
namespace HeartRateMonitorAndroid.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 通知服务接口
|
||||
/// </summary>
|
||||
public interface INotificationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化通知服务
|
||||
/// </summary>
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// 显示心率通知
|
||||
/// </summary>
|
||||
/// <param name="currentHeartRate">当前心率</param>
|
||||
/// <param name="avgHeartRate">平均心率</param>
|
||||
/// <param name="minHeartRate">最低心率</param>
|
||||
/// <param name="maxHeartRate">最高心率</param>
|
||||
/// <param name="duration">监测时长</param>
|
||||
void ShowHeartRateNotification(int currentHeartRate, double avgHeartRate, int minHeartRate, int maxHeartRate, TimeSpan duration);
|
||||
|
||||
/// <summary>
|
||||
/// 取消通知
|
||||
/// </summary>
|
||||
void CancelNotification();
|
||||
|
||||
/// <summary>
|
||||
/// 显示重连通知
|
||||
/// </summary>
|
||||
/// <param name="title">标题</param>
|
||||
/// <param name="message">消息内容</param>
|
||||
/// <param name="attemptCount">尝试次数</param>
|
||||
void ShowReconnectionNotification(string title, string message, int attemptCount);
|
||||
}
|
||||
}
|
||||
@ -1,119 +1,83 @@
|
||||
namespace HeartRateMonitorAndroid.Services
|
||||
using HeartRateMonitorAndroid.Services.Platform;
|
||||
|
||||
namespace HeartRateMonitorAndroid.Services
|
||||
{
|
||||
// 跨平台通知服务
|
||||
/// <summary>
|
||||
/// 通知服务工厂
|
||||
/// </summary>
|
||||
public static class NotificationService
|
||||
{
|
||||
// 常量定义
|
||||
private const string CHANNEL_ID = "HeartRateMonitorChannel";
|
||||
private const int NOTIFICATION_ID = 100;
|
||||
private static readonly INotificationService _instance;
|
||||
|
||||
// 初始化通知服务
|
||||
public static void Initialize()
|
||||
/// <summary>
|
||||
/// 静态构造函数,根据平台创建对应的通知服务实现
|
||||
/// </summary>
|
||||
static NotificationService()
|
||||
{
|
||||
// 根据平台初始化
|
||||
if (DeviceInfo.Platform == DevicePlatform.Android)
|
||||
{
|
||||
#if ANDROID
|
||||
Platforms.Android.AndroidNotificationHelper.CreateNotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"心率监测",
|
||||
"显示实时心率数据");
|
||||
#endif
|
||||
}
|
||||
_instance = new AndroidNotificationService();
|
||||
else if (DeviceInfo.Platform == DevicePlatform.iOS)
|
||||
{
|
||||
#if IOS
|
||||
// 请求iOS通知权限
|
||||
Platforms.iOS.IosNotificationHelper.RequestNotificationPermission().ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
_instance = new IosNotificationService();
|
||||
else if (DeviceInfo.Platform == DevicePlatform.WinUI)
|
||||
_instance = new WindowsNotificationService();
|
||||
else _instance = new NullNotificationService();
|
||||
// 初始化通知服务
|
||||
_instance.Initialize();
|
||||
}
|
||||
|
||||
// 显示心率通知
|
||||
/// <summary>
|
||||
/// 获取当前平台的通知服务实例
|
||||
/// </summary>
|
||||
public static INotificationService Current => _instance;
|
||||
|
||||
#region 便捷方法
|
||||
|
||||
/// <summary>
|
||||
/// 显示心率通知
|
||||
/// </summary>
|
||||
public static void ShowHeartRateNotification(int currentHeartRate, double avgHeartRate, int minHeartRate, int maxHeartRate, TimeSpan duration)
|
||||
{
|
||||
string title = "心率监测";
|
||||
string content = $"当前心率: {currentHeartRate} bpm 平均: {avgHeartRate:0} bpm";
|
||||
string bigText = $"当前心率: {currentHeartRate} bpm\n监测时长: {duration.Hours:00}:{duration.Minutes:00}:{duration.Seconds:00}\n最低: {minHeartRate} bpm | 最高: {maxHeartRate} bpm";
|
||||
|
||||
if (DeviceInfo.Platform == DevicePlatform.Android)
|
||||
{
|
||||
#if ANDROID
|
||||
Platforms.Android.AndroidNotificationHelper.ShowBigTextNotification(
|
||||
CHANNEL_ID,
|
||||
NOTIFICATION_ID,
|
||||
title,
|
||||
content,
|
||||
bigText,
|
||||
Resource.Drawable.notification_icon_background,
|
||||
true);
|
||||
#endif
|
||||
}
|
||||
else if (DeviceInfo.Platform == DevicePlatform.iOS)
|
||||
{
|
||||
#if IOS
|
||||
Platforms.iOS.IosNotificationHelper.ShowNotification(title, content);
|
||||
#endif
|
||||
}
|
||||
else if (DeviceInfo.Platform == DevicePlatform.WinUI)
|
||||
{
|
||||
#if WINDOWS
|
||||
Platforms.Windows.WindowsNotificationHelper.ShowNotification(title, content);
|
||||
#endif
|
||||
}
|
||||
_instance.ShowHeartRateNotification(currentHeartRate, avgHeartRate, minHeartRate, maxHeartRate, duration);
|
||||
}
|
||||
|
||||
// 取消通知
|
||||
/// <summary>
|
||||
/// 取消通知
|
||||
/// </summary>
|
||||
public static void CancelNotification()
|
||||
{
|
||||
if (DeviceInfo.Platform == DevicePlatform.Android)
|
||||
{
|
||||
#if ANDROID
|
||||
Platforms.Android.AndroidNotificationHelper.CancelNotification(NOTIFICATION_ID);
|
||||
#endif
|
||||
}
|
||||
else if (DeviceInfo.Platform == DevicePlatform.iOS)
|
||||
{
|
||||
#if IOS
|
||||
Platforms.iOS.IosNotificationHelper.CancelAllNotifications();
|
||||
#endif
|
||||
}
|
||||
else if (DeviceInfo.Platform == DevicePlatform.WinUI)
|
||||
{
|
||||
#if WINDOWS
|
||||
Platforms.Windows.WindowsNotificationHelper.CancelNotification();
|
||||
#endif
|
||||
}
|
||||
_instance.CancelNotification();
|
||||
}
|
||||
|
||||
// 显示重连通知
|
||||
/// <summary>
|
||||
/// 显示重连通知
|
||||
/// </summary>
|
||||
public static void ShowReconnectionNotification(string title, string message, int attemptCount)
|
||||
{
|
||||
const int RECONNECTION_NOTIFICATION_ID = 101; // 使用不同的ID,避免覆盖心率通知
|
||||
_instance.ShowReconnectionNotification(title, message, attemptCount);
|
||||
}
|
||||
|
||||
if (DeviceInfo.Platform == DevicePlatform.Android)
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// 空实现,用于不支持的平台
|
||||
/// </summary>
|
||||
private class NullNotificationService : INotificationService
|
||||
{
|
||||
#if ANDROID
|
||||
Platforms.Android.AndroidNotificationHelper.ShowNormalNotification(
|
||||
CHANNEL_ID,
|
||||
RECONNECTION_NOTIFICATION_ID,
|
||||
title,
|
||||
message,
|
||||
Resource.Drawable.notification_icon_background,
|
||||
false); // 不使用前台服务,只显示普通通知
|
||||
#endif
|
||||
public void Initialize() { }
|
||||
|
||||
public void ShowHeartRateNotification(int currentHeartRate, double avgHeartRate, int minHeartRate, int maxHeartRate, TimeSpan duration)
|
||||
{
|
||||
// 空实现
|
||||
}
|
||||
else if (DeviceInfo.Platform == DevicePlatform.iOS)
|
||||
|
||||
public void CancelNotification()
|
||||
{
|
||||
#if IOS
|
||||
Platforms.iOS.IosNotificationHelper.ShowNotification(title, message);
|
||||
#endif
|
||||
// 空实现
|
||||
}
|
||||
else if (DeviceInfo.Platform == DevicePlatform.WinUI)
|
||||
|
||||
public void ShowReconnectionNotification(string title, string message, int attemptCount)
|
||||
{
|
||||
#if WINDOWS
|
||||
Platforms.Windows.WindowsNotificationHelper.ShowNotification(title, message);
|
||||
#endif
|
||||
// 空实现
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
72
Services/Platform/AndroidNotificationService.cs
Normal file
72
Services/Platform/AndroidNotificationService.cs
Normal file
@ -0,0 +1,72 @@
|
||||
namespace HeartRateMonitorAndroid.Services.Platform
|
||||
{
|
||||
/// <summary>
|
||||
/// Android平台通知服务实现
|
||||
/// </summary>
|
||||
public class AndroidNotificationService : INotificationService
|
||||
{
|
||||
private const string CHANNEL_ID = "HeartRateMonitorChannel";
|
||||
private const int NOTIFICATION_ID = 100;
|
||||
private const int RECONNECTION_NOTIFICATION_ID = 101;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化通知服务
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
#if ANDROID
|
||||
Platforms.Android.AndroidNotificationHelper.CreateNotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"心率监测",
|
||||
"显示实时心率数据");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示心率通知
|
||||
/// </summary>
|
||||
public void ShowHeartRateNotification(int currentHeartRate, double avgHeartRate, int minHeartRate, int maxHeartRate, TimeSpan duration)
|
||||
{
|
||||
string title = "心率监测";
|
||||
string content = $"当前心率: {currentHeartRate} bpm 平均: {avgHeartRate:0} bpm";
|
||||
string bigText = $"当前心率: {currentHeartRate} bpm\n监测时长: {duration.Hours:00}:{duration.Minutes:00}:{duration.Seconds:00}\n最低: {minHeartRate} bpm | 最高: {maxHeartRate} bpm";
|
||||
|
||||
#if ANDROID
|
||||
Platforms.Android.AndroidNotificationHelper.ShowBigTextNotification(
|
||||
CHANNEL_ID,
|
||||
NOTIFICATION_ID,
|
||||
title,
|
||||
content,
|
||||
bigText,
|
||||
Resource.Drawable.notification_icon_background,
|
||||
true);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消通知
|
||||
/// </summary>
|
||||
public void CancelNotification()
|
||||
{
|
||||
#if ANDROID
|
||||
Platforms.Android.AndroidNotificationHelper.CancelNotification(NOTIFICATION_ID);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示重连通知
|
||||
/// </summary>
|
||||
public void ShowReconnectionNotification(string title, string message, int attemptCount)
|
||||
{
|
||||
#if ANDROID
|
||||
Platforms.Android.AndroidNotificationHelper.ShowNormalNotification(
|
||||
CHANNEL_ID,
|
||||
RECONNECTION_NOTIFICATION_ID,
|
||||
title,
|
||||
message,
|
||||
Resource.Drawable.notification_icon_background,
|
||||
false); // 不使用前台服务,只显示普通通知
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Services/Platform/IosNotificationService.cs
Normal file
52
Services/Platform/IosNotificationService.cs
Normal file
@ -0,0 +1,52 @@
|
||||
namespace HeartRateMonitorAndroid.Services.Platform
|
||||
{
|
||||
/// <summary>
|
||||
/// iOS平台通知服务实现
|
||||
/// </summary>
|
||||
public class IosNotificationService : INotificationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化通知服务
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
#if IOS
|
||||
// 请求iOS通知权限
|
||||
Platforms.iOS.IosNotificationHelper.RequestNotificationPermission().ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示心率通知
|
||||
/// </summary>
|
||||
public void ShowHeartRateNotification(int currentHeartRate, double avgHeartRate, int minHeartRate, int maxHeartRate, TimeSpan duration)
|
||||
{
|
||||
string title = "心率监测";
|
||||
string content = $"当前心率: {currentHeartRate} bpm 平均: {avgHeartRate:0} bpm";
|
||||
|
||||
#if IOS
|
||||
Platforms.iOS.IosNotificationHelper.ShowNotification(title, content);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消通知
|
||||
/// </summary>
|
||||
public void CancelNotification()
|
||||
{
|
||||
#if IOS
|
||||
Platforms.iOS.IosNotificationHelper.CancelAllNotifications();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示重连通知
|
||||
/// </summary>
|
||||
public void ShowReconnectionNotification(string title, string message, int attemptCount)
|
||||
{
|
||||
#if IOS
|
||||
Platforms.iOS.IosNotificationHelper.ShowNotification(title, message);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
49
Services/Platform/WindowsNotificationService.cs
Normal file
49
Services/Platform/WindowsNotificationService.cs
Normal file
@ -0,0 +1,49 @@
|
||||
namespace HeartRateMonitorAndroid.Services.Platform
|
||||
{
|
||||
/// <summary>
|
||||
/// Windows平台通知服务实现
|
||||
/// </summary>
|
||||
public class WindowsNotificationService : INotificationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化通知服务
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
// Windows平台不需要特殊初始化
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示心率通知
|
||||
/// </summary>
|
||||
public void ShowHeartRateNotification(int currentHeartRate, double avgHeartRate, int minHeartRate, int maxHeartRate, TimeSpan duration)
|
||||
{
|
||||
string title = "心率监测";
|
||||
string content = $"当前心率: {currentHeartRate} bpm 平均: {avgHeartRate:0} bpm";
|
||||
|
||||
#if WINDOWS
|
||||
Platforms.Windows.WindowsNotificationHelper.ShowNotification(title, content);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消通知
|
||||
/// </summary>
|
||||
public void CancelNotification()
|
||||
{
|
||||
#if WINDOWS
|
||||
Platforms.Windows.WindowsNotificationHelper.CancelNotification();
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 显示重连通知
|
||||
/// </summary>
|
||||
public void ShowReconnectionNotification(string title, string message, int attemptCount)
|
||||
{
|
||||
#if WINDOWS
|
||||
Platforms.Windows.WindowsNotificationHelper.ShowNotification(title, message);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
226
UI/HeartRateGraphDrawable.cs
Normal file
226
UI/HeartRateGraphDrawable.cs
Normal file
@ -0,0 +1,226 @@
|
||||
using HeartRateMonitorAndroid.Models;
|
||||
using Microsoft.Maui.Graphics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace HeartRateMonitorAndroid.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 心率图表绘制类
|
||||
/// </summary>
|
||||
public class HeartRateGraphDrawable : IDrawable
|
||||
{
|
||||
private List<HeartRateDataPoint> _dataPoints = [];
|
||||
private int _maxPoints = 100; // 最多显示100个数据点
|
||||
private int _minHeartRate = 40;
|
||||
private int _maxHeartRate = 180;
|
||||
|
||||
// 图表配色方案
|
||||
private readonly Color _backgroundColor = Color.FromArgb("#F8F9FA"); // 浅灰背景色
|
||||
private readonly Color _gridLineColor = Color.FromArgb("#E9ECEF"); // 网格线颜色
|
||||
private readonly Color _axisColor = Color.FromArgb("#CED4DA"); // 坐标轴颜色
|
||||
private readonly Color _textColor = Color.FromArgb("#6C757D"); // 文本颜色
|
||||
private readonly Color _heartRateLineColor = Color.FromArgb("#FF4757"); // 心率线颜色
|
||||
private readonly Color _heartRateAreaColor = Color.FromRgba(255, 71, 87, 0.2); // 心率区域填充颜色
|
||||
private readonly Color _heartRatePointColor = Color.FromArgb("#FF4757"); // 数据点颜色
|
||||
private readonly Color _accentColor = Color.FromArgb("#2E86DE"); // 强调色
|
||||
|
||||
public void UpdateData(List<HeartRateDataPoint> dataPoints)
|
||||
{
|
||||
_dataPoints = dataPoints.ToList();
|
||||
// 如果有数据,动态调整Y轴范围
|
||||
if (_dataPoints.Count > 0)
|
||||
{
|
||||
_minHeartRate = Math.Max(40, _dataPoints.Min(p => p.HeartRate) - 10);
|
||||
_maxHeartRate = Math.Min(200, _dataPoints.Max(p => p.HeartRate) + 10);
|
||||
|
||||
// 确保Y轴范围合理
|
||||
int range = _maxHeartRate - _minHeartRate;
|
||||
if (range < 30) // 如果范围太小,扩大它
|
||||
{
|
||||
_minHeartRate = Math.Max(40, _minHeartRate - (30 - range) / 2);
|
||||
_maxHeartRate = Math.Min(200, _maxHeartRate + (30 - range) / 2);
|
||||
}
|
||||
|
||||
// 圆整到最接近的10
|
||||
_minHeartRate = (_minHeartRate / 10) * 10;
|
||||
_maxHeartRate = ((_maxHeartRate + 9) / 10) * 10;
|
||||
}
|
||||
}
|
||||
|
||||
public void Draw(ICanvas canvas, RectF dirtyRect)
|
||||
{
|
||||
// 设置背景色
|
||||
canvas.FillColor = _backgroundColor;
|
||||
canvas.FillRectangle(dirtyRect);
|
||||
|
||||
if (_dataPoints.Count < 2) return; // 至少需要两个点才能绘制线条
|
||||
|
||||
// 计算绘图区域,增加左侧留白以放置y轴标签
|
||||
float leftPadding = 45;
|
||||
float rightPadding = 20;
|
||||
float topPadding = 30;
|
||||
float bottomPadding = 40;
|
||||
|
||||
float graphWidth = dirtyRect.Width - leftPadding - rightPadding;
|
||||
float graphHeight = dirtyRect.Height - topPadding - bottomPadding;
|
||||
float graphBottom = dirtyRect.Height - bottomPadding;
|
||||
float graphTop = topPadding;
|
||||
float graphLeft = leftPadding;
|
||||
float graphRight = dirtyRect.Width - rightPadding;
|
||||
|
||||
// 绘制背景和边框
|
||||
canvas.FillColor = Colors.White;
|
||||
canvas.FillRoundedRectangle(graphLeft - 5, graphTop - 5, graphWidth + 10, graphHeight + 10, 8);
|
||||
canvas.StrokeColor = _gridLineColor;
|
||||
canvas.StrokeSize = 1;
|
||||
canvas.DrawRoundedRectangle(graphLeft - 5, graphTop - 5, graphWidth + 10, graphHeight + 10, 8);
|
||||
|
||||
// 绘制网格线
|
||||
canvas.StrokeColor = _gridLineColor;
|
||||
canvas.StrokeSize = 1;
|
||||
canvas.StrokeDashPattern = new float[] { 4, 4 }; // 虚线网格
|
||||
|
||||
// 水平网格线和心率刻度
|
||||
int yStep = (_maxHeartRate - _minHeartRate) > 100 ? 40 : 20; // 根据范围动态调整步长
|
||||
for (int hr = _minHeartRate; hr <= _maxHeartRate; hr += yStep)
|
||||
{
|
||||
float y = graphBottom - ((hr - _minHeartRate) * graphHeight / (_maxHeartRate - _minHeartRate));
|
||||
|
||||
// 绘制网格线
|
||||
canvas.DrawLine(graphLeft, y, graphRight, y);
|
||||
|
||||
// 绘制心率刻度
|
||||
canvas.FontSize = 12;
|
||||
canvas.FontColor = _textColor;
|
||||
canvas.DrawString(hr.ToString(), graphLeft - 25, y, HorizontalAlignment.Center);
|
||||
}
|
||||
|
||||
// 重置虚线模式
|
||||
canvas.StrokeDashPattern = null;
|
||||
|
||||
// 时间刻度线和标签
|
||||
if (_dataPoints.Count > 0)
|
||||
{
|
||||
int pointCount = _dataPoints.Count;
|
||||
int xStep = Math.Max(1, pointCount / 5); // 大约显示5个时间点
|
||||
|
||||
for (int i = 0; i < pointCount; i += xStep)
|
||||
{
|
||||
if (i >= pointCount) break;
|
||||
float x = graphLeft + (i * graphWidth / (pointCount - 1));
|
||||
|
||||
// 绘制垂直网格线
|
||||
canvas.StrokeColor = _gridLineColor;
|
||||
canvas.StrokeDashPattern = [4, 4];
|
||||
canvas.DrawLine(x, graphTop, x, graphBottom);
|
||||
canvas.StrokeDashPattern = null;
|
||||
|
||||
// 绘制时间刻度(分钟:秒)
|
||||
canvas.FontSize = 12;
|
||||
canvas.FontColor = _textColor;
|
||||
string timeLabel = _dataPoints[i].Timestamp.ToString("mm:ss");
|
||||
canvas.DrawString(timeLabel, x, graphBottom + 15, HorizontalAlignment.Center);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制坐标轴
|
||||
canvas.StrokeColor = _axisColor;
|
||||
canvas.StrokeSize = 2;
|
||||
canvas.DrawLine(graphLeft, graphBottom, graphRight, graphBottom); // X轴
|
||||
canvas.DrawLine(graphLeft, graphTop, graphLeft, graphBottom); // Y轴
|
||||
|
||||
// 添加标题
|
||||
canvas.FontColor = _accentColor;
|
||||
canvas.FontSize = 14;
|
||||
canvas.DrawString("心率监测图表", dirtyRect.Width / 2, graphTop - 15, HorizontalAlignment.Center);
|
||||
|
||||
// 创建心率曲线路径
|
||||
PathF linePath = new PathF();
|
||||
PathF areaPath = new PathF();
|
||||
bool isFirst = true;
|
||||
|
||||
// 添加区域填充起始点
|
||||
areaPath.MoveTo(graphLeft, graphBottom);
|
||||
|
||||
for (int i = 0; i < _dataPoints.Count; i++)
|
||||
{
|
||||
float x = graphLeft + (i * graphWidth / (_dataPoints.Count - 1));
|
||||
float y = graphBottom - ((_dataPoints[i].HeartRate - _minHeartRate) * graphHeight /
|
||||
(_maxHeartRate - _minHeartRate));
|
||||
|
||||
if (isFirst)
|
||||
{
|
||||
linePath.MoveTo(x, y);
|
||||
areaPath.LineTo(x, y);
|
||||
isFirst = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 使用曲线而不是直线,使图表更平滑
|
||||
if (i > 0 && i < _dataPoints.Count - 1)
|
||||
{
|
||||
float prevX = graphLeft + ((i - 1) * graphWidth / (_dataPoints.Count - 1));
|
||||
float prevY = graphBottom - ((_dataPoints[i - 1].HeartRate - _minHeartRate) * graphHeight /
|
||||
(_maxHeartRate - _minHeartRate));
|
||||
float nextX = graphLeft + ((i + 1) * graphWidth / (_dataPoints.Count - 1));
|
||||
float nextY = graphBottom - ((_dataPoints[i + 1].HeartRate - _minHeartRate) * graphHeight /
|
||||
(_maxHeartRate - _minHeartRate));
|
||||
|
||||
float cpx1 = prevX + (x - prevX) * 0.5f;
|
||||
float cpy1 = prevY;
|
||||
float cpx2 = x - (x - prevX) * 0.5f;
|
||||
float cpy2 = y;
|
||||
|
||||
linePath.CurveTo(cpx1, cpy1, cpx2, cpy2, x, y);
|
||||
areaPath.CurveTo(cpx1, cpy1, cpx2, cpy2, x, y);
|
||||
}
|
||||
else
|
||||
{
|
||||
linePath.LineTo(x, y);
|
||||
areaPath.LineTo(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 完成区域填充路径
|
||||
areaPath.LineTo(graphLeft + graphWidth, graphBottom);
|
||||
areaPath.LineTo(graphLeft, graphBottom);
|
||||
areaPath.Close();
|
||||
|
||||
// 绘制区域填充
|
||||
canvas.FillColor = _heartRateAreaColor;
|
||||
canvas.FillPath(areaPath);
|
||||
|
||||
// 绘制曲线
|
||||
canvas.StrokeColor = _heartRateLineColor;
|
||||
canvas.StrokeSize = 3;
|
||||
canvas.DrawPath(linePath);
|
||||
|
||||
// 只绘制最新数据点
|
||||
if (_dataPoints.Count > 0)
|
||||
{
|
||||
// 获取最新数据点的位置
|
||||
int lastIndex = _dataPoints.Count - 1;
|
||||
float x = graphLeft + (lastIndex * graphWidth / (_dataPoints.Count - 1));
|
||||
float y = graphBottom - ((_dataPoints[lastIndex].HeartRate - _minHeartRate) * graphHeight /
|
||||
(_maxHeartRate - _minHeartRate));
|
||||
|
||||
// 绘制最新点的标记
|
||||
canvas.FillColor = _heartRatePointColor;
|
||||
canvas.FillCircle(x, y, 6);
|
||||
canvas.StrokeSize = 2;
|
||||
canvas.StrokeColor = Colors.White;
|
||||
canvas.DrawCircle(x, y, 6);
|
||||
|
||||
// 显示最新心率值
|
||||
canvas.FontSize = 12;
|
||||
canvas.FontColor = _heartRateLineColor;
|
||||
//canvas.Font = FontAttributes.Bold;
|
||||
canvas.DrawString(_dataPoints[lastIndex].HeartRate + " bpm",
|
||||
x, y - 15, HorizontalAlignment.Center);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user