Освой Android играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
Датчики, следящие за физическими свойствами и состоянием окружающей среды, предоставляют инновационные способы для улучшения мобильных приложений. Наличие в современных телефонах электронных компасов, датчиков равновесия, яркости и близости открывает целый ряд новых возможностей для взаимодействия с устройством, таких как дополненная реальность и ввод данных, основанный на перемещениях в пространстве.
Датчики в Android делятся на несколько категорий: движения, положения и окружающей среды. Ниже перечислены некоторые виды популярных датчиков:
В каждом телефоне может быть свой набор датчиков. В большинстве аппаратов есть — акселерометр и гироскоп.
Каждый из представленных датчиков заслуживает отдельной статьи. Имейте в виду, что существуют устаревшие классы для работы с датчиками, в частности, для датчиков ориентации и температуры.
Необходимо помнить несколько вещей, работая с датчиками:
На эмуляторе практически невозможно тестировать работу с датчиками, поэтому используйте реальные устройства. В последних версиях эмуляторов список возможностей датчиков расширился. Смотрите в настройках эмулятора раздел Virtual sensors.
За работу с сенсорами отвечает класс SensorManager, содержащий несколько констант, которые характеризуют различные аспекты системы датчиков Android, в том числе:
Кроме аппаратных датчиков, в устройствах используются виртуальные датчики, которые предоставляют упрощённые, уточнённые или комбинированные показания, используя комбинацию из нескольких аппаратных датчиков. В некоторых случаях этот способ удобнее.
Чтобы получить доступ к сенсорам, нужно вызвать метод getSystemService().
// Kotlin
private lateinit var sensorManager: SensorManager
sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
// Java
private SensorManager sensorManager;
sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
Устройство может включать в себя несколько реализаций одного и того же типа датчиков. Чтобы найти реализацию, используемую по умолчанию, вызовите метод getDefaultSensor() из объекта SensorManager, передавая ему в качестве параметра тип датчика в виде одной из констант, описанных выше.
Следующий фрагмент кода вернёт объект, описывающий гироскоп по умолчанию. Если для данного типа не существует датчика по умолчанию, будет возвращено значение null.
// Kotlin
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
// лучше использовать вариант с проверкой
if (sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null) {
// Успешно! У нас есть гироскоп
} else {
// Неудачно! Гироскоп не обнаружен
}
// Java
Sensor defaultGyroscope = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
Тип датчика | Количество значений | Содержание значений | Примечание |
---|---|---|---|
TYPE_ACCELEROMETER | 3 | value[0]: ось X (поперечная) value[1]: ось Y (продольная) value[2]: ось Z (вертикальная) | Ускорение (м/с2) по трём осям. Константы SensorManager.GRAVITY_* |
TYPE_GRAVITY | 3 | value[0]: ось X (поперечная) value[1]: ось Y (продольная) value[2]: ось Z (вертикальная) | Сила тяжести (м/с2) по трём осям. Константы SensorManager.GRAVITY_* |
TYPE_RELATIVE_HUMIDITY | 1 | value[0]:относительная влажность | Относительная влажность в процентах (%) |
TYPE_LINEAR_ACCELERATION | 3 | value[0]: ось X (поперечная) value[1]: ось Y (продольная) value[2]: ось Z (вертикальная) | Линейное ускорение (м/с2) по трём осям без учёта силы тяжести |
TYPE_GYROSCOPE | 3 | value[0]: ось X value[1]:ось Y value[2]: ось Z | Скорость вращения (рад/с) по трём осям |
TYPE_ROTATION_VECTOR | 4 | values[0]: x*sin(q/2) values[1]: y*sin(q/2) values[2]: z*sin(q/2) values[3]: cos(q/2) | Положение устройства в пространстве. Описывается в виде угла поворота относительно оси в градусах |
TYPE_MAGNETIC_FIELD | 3 | value[0]: ось X (поперечная) value[1]: ось Y (продольная) value[2]: ось Z (вертикальная) | Внешнее магнитное поле (мкТл) |
TYPE_LIGHT | 1 | value[0]: освещённость | Внешняя освещённость (лк). Константы SensorManager.LIGHT_* |
TYPE_PRESSURE | 1 | value[0]: атм.давление | Атмосферное давление (мбар) |
TYPE_PROXIMITY | 1 | value[0]: расстояние | Расстояние до цели |
TYPE_AMBIENT_TEMPERATURE | 1 | value[0]: температура | Температура воздуха в градусах по Цельсию |
TYPE_POSE_6DOF | 15 | см. документацию | |
TYPE_STATIONARY_DETECT | 1 | value[0] | 5 секунд неподвижен |
TYPE_MOTION_DETECT | 1 | value[0] | В движении за последние 5 секунд |
TYPE_HEART_BEAT | 1 | value[0] |
У класса SensorManager есть метод getSensorList(), позволяющий получить список доступных датчиков на устройстве через константу Sensor.TYPE_ALL и метод getName():
// Kotlin
// Если этот код работает, его написал Александр Климов,
// а если нет, то не знаю, кто его писал.
package ru.alexanderklimov.sensors
import android.hardware.Sensor
import android.hardware.SensorManager
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private lateinit var sensorManager: SensorManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
val deviceSensors: List<Sensor> = sensorManager.getSensorList(Sensor.TYPE_ALL)
println(deviceSensors.joinToString("\n"))
// Выводятся данные в таком формате
// {Sensor name="MPL Accelerometer", vendor="InvenSense", version=1, type=1, maxRange=39.226593, resolution=0.0011901855, power=0.5, minDelay=5000}
}
}
// Java
package ru.alexanderklimov.sensors;
import java.util.ArrayList;
import java.util.List;
import android.app.ListActivity;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.widget.ArrayAdapter;
public class SensorsActivity extends ListActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
List<Sensor> deviceSensors = sensorManager.getSensorList(Sensor.TYPE_ALL);
List<String> listSensorType = new ArrayList<>();
for (int i = 0; i < deviceSensors.size(); i++) {
listSensorType.add(deviceSensors.get(i).getName());
}
setListAdapter(new ArrayAdapter<>(this,
android.R.layout.simple_list_item_1, listSensorType));
getListView().setTextFilterEnabled(true);
}
}
Так как у каждого устройства свой набор датчиков, то результаты будут у всех разными. Ниже приведены скриншоты с эмулятора и с реального устройства. В первом случае вывелось только 5 датчиков, во-втором было гораздо больше - вы видите только ту часть, которая поместилась на экране.
Также можно получить список доступных датчиков конкретного типа. В следующем фрагменте кода будут возвращены объекты Sensor, представляющие собой все доступные датчики давления:
// Kotlin
val pressureSensors: List<Sensor> = sensorManager.getSensorList(Sensor.TYPE_PRESSURE)
// Java
List<Sensor> pressureSensors = sensorManager.getSensorList(Sensor.TYPE_PRESSURE);
Можно составить сложное условие, которое будет проверять производителя датчика и его версию. Если необходимого датчика не окажется, то выберем альтернативный вариант.
// Kotlin
if (sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) != null) {
val gravitySensors: List<Sensor> = sensorManager.getSensorList(Sensor.TYPE_GRAVITY)
// Используем конкретный датчик от производителя и нужной версии
sensor = gravitySensors.firstOrNull { it.vendor.contains("Qualcomm") && it.version == 1 }
println(sensor?.vendor)
}
if (sensor == null) {
// Используем акселерометр
sensor = if (sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null) {
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
} else {
// Акселеромент не обнаружен!
null
}
}
Также вам понадобится интерфейс android.hardware.SensorListener. Интерфейс реализован с помощью класса, который используется для ввода значений датчиков по мере их изменения в режиме реального времени. Приложение реализует этот интерфейс для мониторинга одного или нескольких имеющихся аппаратных датчиков.
Интерфейс включает в себя два необходимых метода:
Служба датчиков вызывает onSensorChanged() каждый раз при изменении значений. Все датчики возвращают массив значений с плавающей точкой. Размер массива зависит от особенностей датчика. Датчик TYPE_TEMPERATURE возвращает одно значение - температуру в градусах Цельсия, другие могут возвращать несколько значений. Вы можете использовать только нужные значения. Например, для получения сведений только о магнитном азимуте достаточно использовать первое числов, возвращаемое датчиком TYPE_ORIENTATION.
Параметр accuracy, используемый в методах для представления степени точности датчика, использует одну из констант
Чтобы получать события, генерируемые датчиками, зарегистрируйте свою реализацию интерфейса SensorEventListener с помощью SensorManager. Укажите объект Sensor, за которым вы хотите наблюдать, и частоту, с которой вам необходимо получать обновления.
После получения объекта вы вызываете метод registerListener() в методе onResume(), чтобы начать получать обновлённые данные, и вызываете unregisteredListener() в методе onPause(), чтобы остановить получение данных. В этом случае датчики будут использоваться только тогда, когда активность видна на экране.
В следующем примере показан процесс регистрации SensorEventListener для датчика приближенности по умолчанию с указанием стандартной частоты обновления:
Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
sensorManager.registerListener(mySensorEventListener,
sensor,
SensorManager.SENSOR_DELAY_NORMAL);
Класс SensorManager содержит следующие константы для выбора подходящей частоты обновлений (в порядке убывания):
Выбранная вами частота необязательно будет соблюдаться. SensorManager может возвращать результаты быстрее или медленней, чем вы указали (хотя, как правило, это происходит быстрее). Чтобы минимизировать расход ресурсов при использовании датчиков в приложении, необходимо пытаться подбирать наиболее низкую частоту.
В Android 7.0 Nougat (API 24) появилось понятие динамических датчиков, рассчитанных на платформу Android Things. Датчики могут присоединяться и отсоединяться от платы в любое время.
Для определения доступных динамических датчиков используются методы isDynamicSensorDiscoverySupported(), isDynamicSensor(), getDynamicSensorList().
Момент присоединения или отсоединения датчика от платы можно отслеживать через класс SensorManager.DynamicSensorCallback.
Напоследок стоит упомянуть, что последние версии Android не позволяют получать данные с датчиков в фоне.
Пример с датчиками для Compose