Как отличить день от ночи, если ты 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.

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

    Метки:
    • +21
    • 24,9k
    • 9
    EastBanc Technologies 64,41
    Компания
    Поделиться публикацией
    Комментарии 9
    • +13
      У вас КДПВ с заключительной перепутаны. Ну, я бы поменял )))
      • +4
        можно считать это бонусом для тех, кто дочитал до конца :)
      • +1
        «Как отличить день от ночи, если ты Android» — посчитать угол места Солнца.

        И да, я читал стать и знаю, что вы поставили задачу по-другому.
        • +5
          private final Sensor accelerometer;

          accelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_LIGHT); //Датчик освещённости

          Зачем сенсор освещённости акселерометром обозвали?
          • +3
            Спасибо за бдительность! Поправили:)
          • –2
            В самом первом коде у Вас запятая лишняя

            public class SensorActivity extends Activity, implements SensorEventListener {
            • +1
              50 люкс в жилом помещении это печаль. Читать точно нельзя при такой освещённости обычные книги. Желательно иметь 150-200. А 50 это под кладовку только сойдёт.
              • +1
                Согласен, по современным СП/СНиП 150 лк нормируемая освещенность жилой комнаты.
                Для всяких офисных помещений — 300 лк, для проектных чертежых контор — 400 лк
                • 0
                  Так это вроде пороговое значение для перехода в ночной режим, если я правильно прочитал. Но освещенности в жилой комнате это действительно не соответствует, я бы эту фразу убрал.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое