Как отличить день от ночи, если ты Android

от автора


Привет, Хабр.

Сегодня мы поговорим о том, как здорово читать в темноте. В детстве нам всем мамы запрещали это делать, но теперь есть планшеты! В отличие от бумажных книг, на них не надо светить фонариком, они сами за вас все сделают. И именно мы их этому обучаем. Однако обо всем по порядку.


В одном из мобильных приложений под Android, которое мы разработали, есть экран для чтения новостей. Для удобства пользователей мы предусмотрели в нём два режима отображения – дневной и ночной. Всё просто: если устройство «знает», что сейчас день (или просто светло), – работает обычный экран, с чёрным шрифтом на белом. Если же оно понимает, что пользователь в темноте, – предлагает ему переключиться в ночной режим – белый шрифт и чёрный экран.

Самое главное – это вовремя предложить пользователю сделать переключение. Для этого и нужно определять, день сейчас или ночь с помощью сенсора устройства.

Работа с любым сенсором в Android сводится к следующим шагам:
1. Получить доступ к SensorManager.
2. Получить доступ к желаемому сенсору.
3. Зарегистрировать listener, используя общий для всех сенсоров интерфейс.

Пример работы с SensorManager:

public class SensorActivity extends Activity, implements SensorEventListener {      private final SensorManager sensorManager;      private final Sensor accelerometer;        public SensorActivity() {          sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);          accelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); //Датчик освещённости      }        protected void onResume() {          super.onResume();          sensorManager.registerListener(this, accelerometer, 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.

Ну вот и готово, реализация достаточно простая. Здорово, что под бездушным кодом скрывается связь с физикой. Используя сенсоры, которыми оснащено устройство, мы можем сделать приложение удобней для пользователя и в какой- то степени интерактивным. Теперь можно не задумываясь продолжать чтение, независимо от наступления дня или ночи, заезда в тоннель или выхода на пляж.
Тем более, лето на подходе – все бегом на пляж читать.

ссылка на оригинал статьи http://habrahabr.ru/post/255961/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *