Как отличить день от ночи, если ты Android
Привет, Хабр. Сегодня мы поговорим о том, как здорово читать в темноте. В детстве нам всем мамы запрещали это делать, но теперь есть планшеты! В отличие от бумажных книг, на них не надо светить фонариком, они сами за вас все сделают. И именно мы их этому обучаем. Однако обо всем по порядку.
В одном из мобильных приложений под Android, которое мы разработали, есть экран для чтения новостей. Для удобства пользователей мы предусмотрели в нём два режима отображения — дневной и ночной. Всё просто: если устройство «знает», что сейчас день (или просто светло), — работает обычный экран, с чёрным шрифтом на белом. Если же оно понимает, что пользователь в темноте, — предлагает ему переключиться в ночной режим — белый шрифт и чёрный экран.
Самое главное — это вовремя предложить пользователю сделать переключение. Для этого и нужно определять, день сейчас или ночь с помощью сенсора устройства.
Работа с любым сенсором в Android сводится к следующим шагам:1. Получить доступ к SensorManager.2. Получить доступ к желаемому сенсору.3. Зарегистрировать listener, используя общий для всех сенсоров интерфейс.
Пример работы с SensorManager:
public class SensorActivity extends Activity, implements SensorEventListener { private final SensorManager sensorManager; private final Sensor lightSensor; public SensorActivity () { sensorManager = (SensorManager)getSystemService (SENSOR_SERVICE); lightSensor = mSensorManager.getDefaultSensor (Sensor.TYPE_LIGHT); //Датчик освещённости } protected void onResume () { super.onResume (); sensorManager.registerListener (this, lightSensor, SensorManager.SENSOR_DELAY_NORMAL); //Подключаемся к сенсору } protected void onPause () { super.onPause (); sensorManager.unregisterListener (this); //Отключаемся от сенсора } public void onAccuracyChanged (Sensor sensor, int accuracy) { } public void onSensorChanged (SensorEvent event) { //Получаем данные из SensorEvent } } Все данные от сенсора приходят в массиве SensorEvent#values.По документации вот, что присылает нам сенсор освещённости:
Sensor.TYPE_LIGHT: values[0]: Ambient light level in SI lux units
Всего одно значение — количество люксов.
Минутка образования Что такое люкс? Ну, тут всё просто: люкс — это единица освещённости поверхности 1 м² при световом потоке падающего на неё излучения, равном 1 лм (люмен). А люмен — это единица измерения светового потока, равная световому потоку, испускаемому точечным изотропным источником, c силой света, равной одной канделе, в телесный угол величиной в один стерадиан. А стерадиан — это… Впрочем, давайте просто посмотрим на картинку:
(источник blog.tredz.co.uk/wp-content/uploads/2012/09/light-dia1.jpg)Если всё вместе, то люкс — это такая освещённость поверхности в 1 м², которая возникает, когда на неё светят вот такой лампочкой с силой света в 1 кд (кандела) вот таким пучком света размером в 1 стерадиан.OK, количество люксов мы знаем, что дальше? Дальше попытаемся выяснить, какой уровень освещённости типичен для светлого времени суток и для тёмного.К слову сказать, не пытайтесь искать в поисковиках по ключевым словам «люкс», «день», «ночь», если не хотите быть в курсе лучших цен на комфортабельные номера в гостиницах :).
В русской Wiki можно отыскать табличку с примерами освещённости, в которой можно обнаружить такие полезные примеры как: • до 20 — В море на глубине ~50 м.• 350±150 — Восход или закат на Венере
В силу того, что мы делаем приложение не для жителей Венеры, остановимся на значении в 50 люксов, что соответствует освещенности в жилой комнате.
Дело техники Напишем класс LightSensorManager, который можно будет «включать» и «выключать» и который будет рапортовать нам, если стало темно или светло.LightSensorManager public class LightSensorManager implements SensorEventListener { private enum Environment {DAY, NIGHT} public interface EnvironmentChangedListener { void onDayDetected (); void onNightDetected (); } private static final int THRESHOLD_LUX = 50; private static final String TAG = «LightSensorManager»; private final SensorManager sensorManager; private final Sensor lightSensor; private EnvironmentChangedListener environmentChangedListener; private Environment currentEnvironment; public LightSensorManager (Context context) { sensorManager = (SensorManager) context.getSystemService (Context.SENSOR_SERVICE); lightSensor = sensorManager.getDefaultSensor (Sensor.TYPE_LIGHT); // Сенсор освещённости } public void enable () { if (lightSensor!= null){ sensorManager.registerListener (this, lightSensor, SensorManager.SENSOR_DELAY_NORMAL); } else { Log.w (TAG, «Light sensor in not supported»); } } public void disable () { sensorManager.unregisterListener (this); } public EnvironmentChangedListener getEnvironmentChangedListener () { return environmentChangedListener; } public void setEnvironmentChangedListener (EnvironmentChangedListener environmentChangedListener) { this.environmentChangedListener = environmentChangedListener; } @Override public void onSensorChanged (SensorEvent event) { float luxLevel = event.values[0]; Environment oldEnvironment = currentEnvironment; currentEnvironment = luxLevel < THRESHOLD_LUX ? Environment.NIGHT : Environment.DAY; if (hasChanged(oldEnvironment, currentEnvironment)){ callListener(currentEnvironment); } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) {} private boolean hasChanged(Environment oldEnvironment, Environment newEnvironment) { return oldEnvironment != newEnvironment; } private void callListener(Environment environment) { if (environmentChangedListener == null || environment == null){ return; } switch (environment) { case DAY: environmentChangedListener.onDayDetected(); break; case NIGHT: environmentChangedListener.onNightDetected(); break; } } }
Теперь мы можем добавить этот менеджер в нашу Activity, включая его в onResume и выключая в onPause.Вы можете понаблюдать, как меняется уровень освещенности, не выходя из комнаты. Просто найдите датчик на девайсе и закройте его пальцем.Может случиться так, что девайс окажется в комнате с уровнем освещённости примерно равным нашему выбранному пороговому значению в 50 люксов и, колеблясь, будет часто пересекать пороговое значение. Это приведет к тому, что наш менеджер начнет очень часто сообщать нам о смене дня и ночи. Мы избавимся от этого, введя 2 пороговых значения: верхнее и нижнее. Выше верхнего мы будем считать днём, ниже нижнего — ночью, а изменения между порогами будем игнорировать.LightSensorManager с двумя пороговыми значениями public class LightSensorManager implements SensorEventListener { private enum Environment {DAY, NIGHT} public interface EnvironmentChangedListener { void onDayDetected (); void onNightDetected (); } private static final int THRESHOLD_DAY_LUX = 50; private static final int THRESHOLD_NIGHT_LUX = 40; private static final String TAG = «LightSensorManager»; private final SensorManager sensorManager; private final Sensor lightSensor; private EnvironmentChangedListener environmentChangedListener; private Environment currentEnvironment; public LightSensorManager (Context context) { sensorManager = (SensorManager) context.getSystemService (Context.SENSOR_SERVICE); lightSensor = sensorManager.getDefaultSensor (Sensor.TYPE_LIGHT); // Сенсор освещённости } public void enable () { if (lightSensor!= null){ sensorManager.registerListener (this, lightSensor, SensorManager.SENSOR_DELAY_NORMAL); } else { Log.w (TAG, «Light sensor in not supported»); } } public void disable () { sensorManager.unregisterListener (this); } public EnvironmentChangedListener getEnvironmentChangedListener () { return environmentChangedListener; } public void setEnvironmentChangedListener (EnvironmentChangedListener environmentChangedListener) { this.environmentChangedListener = environmentChangedListener; } @Override public void onSensorChanged (SensorEvent event) { float luxLevel = event.values[0]; Environment oldEnvironment = currentEnvironment; if (luxLevel < THRESHOLD_NIGHT_LUX){ currentEnvironment = Environment.NIGHT; } else if (luxLevel > THRESHOLD_DAY_LUX){ currentEnvironment = Environment.DAY; } if (hasChanged (oldEnvironment, currentEnvironment)){ callListener (currentEnvironment); } } @Override public void onAccuracyChanged (Sensor sensor, int accuracy) {} private boolean hasChanged (Environment oldEnvironment, Environment newEnvironment) { return oldEnvironment!= newEnvironment; } private void callListener (Environment environment) { if (environmentChangedListener == null || environment == null){ return; } switch (environment) { case DAY: environmentChangedListener.onDayDetected (); break; case NIGHT: environmentChangedListener.onNightDetected (); break; } } }
И еще один нюанс: мы можем получить ложное срабатывание при кратковременном сильном изменении уровня освещённости. Например, если «моргнёт свет» из-за перепада напряжения или пользователь пройдет ночью под фонарным столбом.
Мы можем избавиться от этой проблемы, если запрограммируем фильтр низких частот (он же low pass filter). Он сгладит все резкие и кратковременные изменения в данных от сенсора.
LightSensorManager с фильтром низких частот public class LightSensorManager implements SensorEventListener { private enum Environment {DAY, NIGHT} public interface EnvironmentChangedListener { void onDayDetected (); void onNightDetected (); } private static final float SMOOTHING = 10; private static final int THRESHOLD_DAY_LUX = 50; private static final int THRESHOLD_NIGHT_LUX = 40; private static final String TAG = «LightSensorManager»; private final SensorManager sensorManager; private final Sensor lightSensor; private EnvironmentChangedListener environmentChangedListener; private Environment currentEnvironment; private final LowPassFilter lowPassFilter; public LightSensorManager (Context context) { sensorManager = (SensorManager) context.getSystemService (Context.SENSOR_SERVICE); lightSensor = sensorManager.getDefaultSensor (Sensor.TYPE_LIGHT); lowPassFilter = new LowPassFilter (SMOOTHING); } public void enable () { if (lightSensor!= null){ sensorManager.registerListener (this, lightSensor, SensorManager.SENSOR_DELAY_NORMAL); } else { Log.w (TAG, «Light sensor in not supported»); } } public void disable () { sensorManager.unregisterListener (this); } public EnvironmentChangedListener getEnvironmentChangedListener () { return environmentChangedListener; } public void setEnvironmentChangedListener (EnvironmentChangedListener environmentChangedListener) { this.environmentChangedListener = environmentChangedListener; } @Override public void onSensorChanged (SensorEvent event) { float luxLevel = event.values[0]; luxLevel = lowPassFilter.submit (luxLevel); Environment oldEnvironment = currentEnvironment; if (luxLevel < THRESHOLD_NIGHT_LUX){ currentEnvironment = Environment.NIGHT; } else if (luxLevel > THRESHOLD_DAY_LUX){ currentEnvironment = Environment.DAY; } if (hasChanged (oldEnvironment, currentEnvironment)){ callListener (currentEnvironment); } } @Override public void onAccuracyChanged (Sensor sensor, int accuracy) {} private boolean hasChanged (Environment oldEnvironment, Environment newEnvironment) { return oldEnvironment!= newEnvironment; } private void callListener (Environment environment) { if (environmentChangedListener == null || environment == null){ return; } switch (environment) { case DAY: environmentChangedListener.onDayDetected (); break; case NIGHT: environmentChangedListener.onNightDetected (); break; } } } public class LowPassFilter { private float filteredValue; private final float smoothing; private boolean firstTime = true; public LowPassFilter (float smoothing) { this.smoothing = smoothing; } public float submit (float newValue){ if (firstTime){ filteredValue = newValue; firstTime = false; return filteredValue; } filteredValue += (newValue — filteredValue) / smoothing; return filteredValue; } } Кстати говоря, разработчики Android любезно добавили в класс SensorManager несколько констант, связанных с разной степенью освещённости, например, SensorManager.LIGHT_CLOUDY или SensorManager.LIGHT_FULLMOON.Ну вот и готово, реализация достаточно простая. Здорово, что под бездушным кодом скрывается связь с физикой. Используя сенсоры, которыми оснащено устройство, мы можем сделать приложение удобней для пользователя и в какой- то степени интерактивным. Теперь можно не задумываясь продолжать чтение, независимо от наступления дня или ночи, заезда в тоннель или выхода на пляж.Тем более, лето на подходе — все бегом на пляж читать.