Содержание
Устройство Android может содержать различные датчики (датчик ускорения, компас и так далее). В .NET MAUI мы можем взаимодействовать с этими датчиками и при необходимости, корректировать работу нашего приложения в зависимости от того или иного показания датчика. Датчики устройства описываются в пространстве имен Microsoft.Maui.Devices.Sensors
, а работа с ними строится практически по одному сценарию. Поэтому имеет смысл рассмотреть работу со всеми возможными датчиками в двух частях: в первой части мы рассмотрим общие моменты, связанные с работой датчиков на устройстве, а во второй — различия.
Скорость обновления показаний датчика
Для всех датчиков необходимо установить скорость обновления данных. В .NET MAUI для этого используется перечисление SensorSpeed
, которое содержит следующие значения:
Имя | Значение | Описание | Интервал обновления |
---|---|---|---|
Default |
0 |
Скорость датчика по умолчанию устройства. | 200 мс |
UI |
1 |
Скорость, подходящая для общего пользовательского интерфейса. | 60 мс. |
Game |
2 |
Подходит для игр. | 20 мс |
Fastest |
3 |
Как можно быстрее получить данные датчика. | 5 мс |
Следует отметить, что, начиная с .NET 8 скорость обновления датчиков одинакова для всех поддерживаемых платформ. Рассмотрим работу с датчиками устройства на примере небольшого приложения .NET MAUI.
Датчики устройства
Определение необходимых разрешений
Для того, чтобы наше приложение могло взаимодействовать с различными видами датчиков устройства, в Android необходимо запросить разрешение android.permission.HIGH_SAMPLING_RATE_SENSORS
. Сделать это можно любым из известных нам способов. Например, создадим новый проект .NET MAUI и используем атрибут сборки для определения разрешения:
using Android.App; using Android.Runtime; [assembly: UsesPermission(Android.Manifest.Permission.HighSamplingRateSensors)] namespace MauiSensors; [Application] public class MainApplication : MauiApplication { public MainApplication(IntPtr handle, JniHandleOwnership ownership) : base(handle, ownership) { } protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); }
Датчики устройства: общие принципы работы
Используя конкретные реализации интерфейсов датчиков работа с ними строится по общему алгоритму:
- Проверяется поддержка того или иного датчика на устройстве. Для этого используется свойство
IsSupported
- Проверяется отслеживаются ли в данный момент показания датчика. Для этого используется свойство
IsMonitoring
- Если показания не отслеживаются, то приложение подписывается на событие датчика
ReadingChanged
(или аналогичное) и вызывается методStart()
с параметром скорости обновления (одним из значенийSensorSpeed
) - Если приложение заканчивает работу, то приложение отписывается от события и вызывается метод
Stop()
.
Конечно, каждый датчик возвращает свои значения, например, акселерометр возвращает вектор ускорения, барометр — давление и т.д. Но принцип чтения данных один и тот же. Теперь перейдем к реализации приложения.
Проверка наличия датчиков устройства и их запуск
Начнем с того, что проверим есть ли какие-либо датчики на устройстве. Чтобы хранить данные о датчиках на устройстве, создадим в проекте папку Models и разместим в ней класс SensorInfo
следующего содержания:
using System.ComponentModel; using System.Runtime.CompilerServices; namespace MauiSensors.Models { public class SensorInfo: INotifyPropertyChanged { public object Sensor { get; set; } public bool CanStart { get; set; } private bool isStarted; public bool IsStarted { get => isStarted; set { if (isStarted != value) { isStarted = value; OnPropertyChanged(); } } } public SensorInfo() { } public SensorInfo(object sensor, bool canStart) { Sensor = sensor; CanStart = canStart; } public event PropertyChangedEventHandler? PropertyChanged; public void OnPropertyChanged([CallerMemberName] string prop = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); } } }
Объекты этого класса будут хранить следующую информацию:
- объект типа, реализующего интерфейс датчика по умолчанию (
Sensor
) - возможность запуска датчика (
CanStart
) — если датчик установлен на устройстве, то свойствоCanStart
будет равноtrue
- текущее состояние датчика (
IsStarted
). ЕслиIsStarted = true
, то датчик работает
Для того, чтобы можно было отслеживать состояние датчика в интерфейсе приложения, мы реализовали в этом классе интерфейс INotifyPropertyChanged
.
Для этого создадим в проекте папку ViewModels и разместим в ней модель представления SensorsViewModel
со следующим содержимым:
using MauiSensors.Models; using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; namespace MauiSensors.ViewModels { public class SensorsViewModel : INotifyPropertyChanged { private List<object> _sensors =[Accelerometer.Default, Compass.Default, Magnetometer.Default, Barometer.Default, Gyroscope.Default, OrientationSensor.Default]; public ObservableCollection<SensorInfo> Sensors { get; set; } = [ new SensorInfo( Accelerometer.Default, false), new SensorInfo( Compass.Default, false), new SensorInfo( Magnetometer.Default, false), new SensorInfo( Barometer.Default, false), new SensorInfo( Gyroscope.Default, false), new SensorInfo( OrientationSensor.Default, false) ]; public ICommand CheckCommand { get; set; } public ICommand StartCommand { get; set; } public SensorsViewModel() { StartCommand = new Command<SensorInfo>((sensorInfo) => { if (sensorInfo.CanStart) { if (sensorInfo.IsStarted == false) { var method = sensorInfo.Sensor.GetType().GetMethod(name: "Start", genericParameterCount: 0, [typeof(SensorSpeed)]); var result = method?.Invoke(sensorInfo.Sensor, [SensorSpeed.Default]); } else { var method = sensorInfo.Sensor.GetType().GetMethod(name: "Stop"); var result = method?.Invoke(sensorInfo.Sensor, null); } } sensorInfo.IsStarted = (bool)sensorInfo.Sensor.GetType().GetProperty("IsMonitoring").GetValue(sensorInfo.Sensor); }); CheckCommand = new Command(() => { Sensors.Clear(); foreach (var sensor in _sensors) { Sensors.Add(new SensorInfo(sensor, (bool)(sensor.GetType().GetProperty("IsSupported").GetValue(sensor)))); } }); } public event PropertyChangedEventHandler? PropertyChanged; public void OnPropertyChanged([CallerMemberName] string prop = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); } } }
Здесь свойство Sensors
содержит все доступные на данный момент в .NET MAUI датчики, с которыми мы можем работать. По умолчанию, для каждого датчика мы устанавливаем свойство CanStart=false
. В модели представления мы определили две команды:
public ICommand CheckCommand { get; set; } public ICommand StartCommand { get; set; }
Первая команда проверяет доступность датчика на устройстве:
CheckCommand = new Command(() => { Sensors.Clear(); foreach (var sensor in _sensors) { Sensors.Add(new SensorInfo(sensor, (bool)(sensor.GetType().GetProperty("IsSupported").GetValue(sensor)))); } });
Так как датчики содержат одинаковые свойства, например, IsSupported
, то мы можем воспользоваться рефлексией, перебрав весь список датчиков и прочитав необходимое нам значение свойства.
Вторая команда — StartCommand
запускает или останавливает датчик. Эту команду мы ещё будем дорабатывать в следующей части. Сейчас мы просто проверяем состояние датчика и, в зависимости от этого, либо запускаем датчик в работу, либо останавливаем его:
if (sensorInfo.CanStart) { if (sensorInfo.IsStarted == false) { var method = sensorInfo.Sensor.GetType().GetMethod(name: "Start", genericParameterCount: 0, [typeof(SensorSpeed)]); var result = method?.Invoke(sensorInfo.Sensor, [SensorSpeed.Default]); } else { var method = sensorInfo.Sensor.GetType().GetMethod(name: "Stop"); var result = method?.Invoke(sensorInfo.Sensor, null); } } sensorInfo.IsStarted = (bool)sensorInfo.Sensor.GetType().GetProperty("IsMonitoring").GetValue(sensorInfo.Sensor);
Здесь стоит обратить внимание на то, как запускается датчик:
var method = sensorInfo.Sensor.GetType().GetMethod(name: "Start", genericParameterCount: 0, [typeof(SensorSpeed)]); var result = method?.Invoke(sensorInfo.Sensor, [SensorSpeed.Default]);
дело в том, что датчик Compass
содержит две версии метода Start()
, а остальные — одну. Поэтому для поиска необходимого метода запуска мы используем метод GetMethod()
с несколькими параметрами. Все датчики мы будем запускать с одинаковой скоростью обновления — SensorSpeed.Default
.
Теперь необходимо разработать визуальный интерфейс нашего приложения. Так как XAML-код страницы будет немного сложнее, чем обычно, то вначале, я покажу как будет выглядеть страница приложения. Вот так будет выглядеть приложение в работе:
Названия датчиков будем получать по типу объекта, который реализует интерфейс датчика. Если датчик доступен и может использоваться то кнопка датчика может быть в двух состояниях — «Старт» и синяя заливка, когда датчик остановлен и «Стоп» и красная заливка, если датчик работает. Если датчик недоступен, то кнопка также становится недоступной.
Для того, чтобы получить внешний вид странице, как показано на рисунке, необходимо использовать два конвертера значений, которые будут располагаться в папке Converters проекта:
using System.Globalization; namespace MauiSensors.Converters { public class SensorTypeToString : IValueConverter { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { string name = value.GetType().Name; return name.Replace("Implementation", ""); } public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
Конвертер SensorTypeToString
берет тип объекта, обрезает суффикс Implementation
из имени типа и возвращает полученную строку. Этот конвертер будет использоваться нами для первого столбца таблицы с датчиками. Второй конвертер:
using System.Globalization; namespace MauiSensors.Converters { class SensorBoolToString : IValueConverter { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { if (parameter == null) return string.Empty; if ((bool)value == false) return (parameter as string[])[0]; else return (parameter as string[])[1]; } public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
Возвращает строку по значению value
и использует в качестве параметра массив из двух строк. Этот конвертер мы будем использовать для второго столбца таблицы — выводить строку «Доступен» или «Недоступен».
Таким образом, на данный момент, структура нашего проекта будет выглядеть следующим образом:
Теперь передадим в качестве контекста страницы MainPage нашу модель представления:
using MauiSensors.ViewModels; namespace MauiSensors { public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); BindingContext = new SensorsViewModel(); } } }
и напишем следующим код XAML для MainPage:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="MauiSensors.MainPage" xmlns:local="clr-namespace:MauiSensors.ViewModels" xmlns:converters="clr-namespace:MauiSensors.Converters" xmlns:models="clr-namespace:MauiSensors.Models" x:DataType="local:SensorsViewModel"> <ContentPage.Resources> <ResourceDictionary> <converters:SensorTypeToString x:Key="SensorTypeToString"/> <converters:SensorBoolToString x:Key="SensorBoolToString"/> <x:Array Type="{x:Type x:String}" x:Key="SensorExistArray"> <x:String>Недоступен</x:String> <x:String>Доступен</x:String> </x:Array> </ResourceDictionary> </ContentPage.Resources> <ScrollView> <VerticalStackLayout Padding="30,0" Spacing="25"> <Button Text="Проверить датчики устройства" Command="{Binding CheckCommand}"/> <CollectionView ItemsSource="{Binding Sensors,Mode=TwoWay}"> <CollectionView.ItemTemplate> <DataTemplate x:DataType="{x:Type models:SensorInfo}"> <Grid RowSpacing="10"> <Grid.RowDefinitions> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="2*"></ColumnDefinition> <ColumnDefinition Width="1.5*"></ColumnDefinition> <ColumnDefinition Width="1.5*"></ColumnDefinition> </Grid.ColumnDefinitions> <Label Text="{Binding Path=Sensor, Converter={StaticResource SensorTypeToString}}" VerticalOptions="Center" Grid.Row="0" Grid.Column="0"/> <Label Text="{Binding Path=CanStart, Converter={StaticResource SensorBoolToString}, ConverterParameter={StaticResource SensorExistArray}}" VerticalOptions="Center" Grid.Row="0" Grid.Column="1" /> <Button Text="Старт" IsEnabled="{Binding CanStart}" Grid.Row="0" Grid.Column="2" Padding="10" Margin="5" Command="{Binding Path=StartCommand, Source={RelativeSource AncestorType={x:Type local:SensorsViewModel}}}" CommandParameter="{Binding}"> <Button.Triggers> <DataTrigger TargetType="Button" Binding="{Binding Path=IsStarted, Mode=TwoWay}" Value="false"> <Setter Property="Text" Value="Старт"></Setter> <Setter Property="Background" Value="Blue"/> </DataTrigger> <DataTrigger TargetType="Button" Binding="{Binding Path=IsStarted, Mode=TwoWay}" Value="true"> <Setter Property="Text" Value="Стоп"></Setter> <Setter Property="Background" Value="Red"></Setter> </DataTrigger> <DataTrigger TargetType="Button" Binding="{Binding Path=CanStart, Mode=TwoWay}" Value="false"> <Setter Property="Text" Value="н/д"></Setter> <Setter Property="Background" Value="LightGray"></Setter> </DataTrigger> </Button.Triggers> </Button> </Grid> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </VerticalStackLayout> </ScrollView> </ContentPage>
В принципе, здесь мы используем то, что уже изучили ранее — привязку к данным произвольного объекта, конвертеры значений, относительные привязки, триггеры данных (для кнопки) и работу с контейнером компоновки Grid
. В качестве закрепления изученного материала, можно рассмотреть самостоятельно этот код по частям, чтобы разобраться с тем, как он работает. А работает он ровно так, как показано на рисунке выше.
Итого
В этой части мы рассмотрели наиболее общие моменты работы с датчиками на устройстве Android — проверили наличие датчика на устройстве, научились запускать и останавливать датчики устройства. В следующей части мы продолжим работу над нашим приложением и научимся считывать показания датчиков.