Density independent pixel (dp) в Android


Вопрос про единицу измерения длины “dp” постоянно появляется на различных форумах и сайтах посвященных разработке приложений под ОС Android.
В большинстве случаев, в качестве ответа более опытные программисты приводят цитату с официального сайта:

Density-independent pixel (dp)
A virtual pixel unit that you should use when defining UI layout, to express layout dimensions or position in a density-independent way.
The density-independent pixel is equivalent to one physical pixel on a 160 dpi screen, which is the baseline density assumed by the system for a “medium” density screen. At runtime, the system transparently handles any scaling of the dp units, as necessary, based on the actual density of the screen in use. The conversion of dp units to screen pixels is simple: px = dp * (dpi / 160). For example, on a 240 dpi screen, 1 dp equals 1.5 physical pixels. You should always use dp units when defining your application’s UI, to ensure proper display of your UI on screens with different densities.

В целом, мотивы создания dp-единицы описаны довольно ясно. Никому не хочется делать множество различных версток для экранов с разным разрешением и плотностью точек.
Тем не менее, давайте посмотрим более подробно на указанную выше формулу:

px = dp * (dpi / 160)

Здесь главное не запутаться. Если px – это длина в пикселях, а dp – длина в д-пикселях, то для dpi = 240 получится:

px = (dpi/160) * dp = 1.5 * dp

По указанной выше формуле получим, что если dp = 1, то px = 1.5.

Попробуем разобрать ситуацию по существу. Ввод единицы измерения длины dp, нужен был для того, чтобы на экранах с разным количество точек на дюйм размеры графических элементов были одинаковыми.
Например, если взять экран с 160dpi (160 точек на линейный дюйм) и с 240dpi, то для того, чтобы физический размер кнопки был одинаковый, нужно на экране с 240dpi размер кнопки в пикселях делать в 1.5 раз больше.

Здесь важно определиться в понятиях размер, разрешение и плотность:

  • Размеры (3”, 10” и т.д.) – физические размеры экрана. В Android-е это разные группы ресурсов: small, normal, large, and extra large. Измеряются в dp.
  • Разрешение (QWGA, HVGA и т.д.) – количество точек на экране (например: 1024×720).
  • Плотность (DPI) – количество точек на линейный дюйм. В Android-е это разные группы: ldpi (low), mdpi (medium), hdpi (high), and xhdpi (extra high).

Итак понятно, что единица dp – это такой виртуальный пиксель, который на плотных экранах занимает большее количество пикселей, а на разреженных меньше.
Цель – сохранить единообразие в пользовательском интерфейсе.

Дальше самое интересное. Чтобы лучше понять, как происходит преобразование dp в px на самом деле, думаю имеет смысл посмотреть в исходный код:

   /**
     * Converts an unpacked complex data value holding a dimension to its final floating 
     * point value. The two parameters unit and value
     * are as in {@link #TYPE_DIMENSION}.
     *  
     * @param unit The unit to convert from.
     * @param value The value to apply the unit to.
     * @param metrics Current display metrics to use in the conversion -- 
     *                supplies display density and scaling information.
     * 
     * @return The complex floating point value multiplied by the appropriate 
     * metrics depending on its unit. 
     */
    public static float applyDimension(int unit, float value,
                                       DisplayMetrics metrics)
    {
        switch (unit) {
        case COMPLEX_UNIT_PX:
            return value;
        case COMPLEX_UNIT_DIP:
            return value * metrics.density;
        case COMPLEX_UNIT_SP:
            return value * metrics.scaledDensity;
        case COMPLEX_UNIT_PT:
            return value * metrics.xdpi * (1.0f/72);
        case COMPLEX_UNIT_IN:
            return value * metrics.xdpi;
        case COMPLEX_UNIT_MM:
            return value * metrics.xdpi * (1.0f/25.4f);
        }
        return 0;
    }

В первую очередь в глаза бросается не соблюдение конвенций – фигурная скобка “{” вынесена на новую строку. Такое форматирование не редко встречается в исходниках андроида. Это несколько подрывает репутацию программистов, занимающихся разработкой этой платформы (никогда не понимал людей, которые не могут нормально отформатировать код, т.к это делается нажатием нескольких кнопок или вообще автоматически).

Попробуем вывести соответствие между длиной в dp и реальной длиной в дюймах.
Пусть:
Ldp – длина в dp
Linch – длина в дюймах
Lpx – длина в пикселях
DPI = metrics.densityDpi – плотность (точек на линейный дюйм), которая может принимать значения только одно из значений: 120, 160, 240, 320 и т.д.
DPIreal = metrics.xdpi – плотность, которая соответствует конкретному устройству. Может отличаться от DPI, например у некоторых устройств реальное metrics.xdpi может быть 180, но при этому будут использоваться файлы для DPI=160 (т.к. DPI не может принимать промежуточные значения, а только равняться одному из строго определенных чисел ).

В итоге получим:
Lpx = (DPI/160) * Ldp =>
Ldp = (160/DPI) * Lpx

Lpx = Linch * DPIreal
из этого следует:
Ldp = (DPIreal/DPI) * (160 * Linch)

Легко видеть, что Ldp равняется приблизительно 1/160 дюйма. Приблизительно – потому, что за счет расхождения между DPI и DPIreal, погрешность может превышать 25% (Например при DPIreal=200 vs DPI=240).

В итоге мы получили очень интересную относительную единицу измерения – dp. Она чем-то напоминает старинные единицу измерения длины основанные на длине ступни, локтя или шага и которые использовались в бытовых целях. Поскольку размеры у всех людей были разные, то и результат измерений сильно отличался. Сейчас гугл создал похожую систему измерений для андроидов.
По моему скромному мнению, она далеко от совершенства, но т.к. Google говорит о том, что нужно использовать именно dp – все будут дружно использовать dp.

Дополнительно ситуацию ухудшает тот факт, что значение dpi и xdpi – прописанные для каждого устройства, могут отличаться от фактических. Такое не редко можно встретить на дешевых устройствах, в которых количество точек на дюйм не соответствует реальному.

Некоторые товарищи меня спрашивают, как вообще можно использовать настолько нестабильную единицу измерений?
Для того, чтобы ответить на этот вопрос, нужно немножко понаблюдать за процессом верстки интерфейса. В начале человек набрасывает элементы, а потом начинает проверять – все ли хорошо? Если нет – добавил пару dp. Если попал, отлично. Нет – добавляет еще. До тех пор пока не надоест или не получится более-менее приемлемый вариант, на данном экране телефона (разрешение,размер, плотность). Со временем количество попаданий увеличивается (за счет интуиции и роста опыта). Поскольку фрагментация у андроида очень высокая, а в добавок в качестве единицы измерения используются “dp”, то верстка интерфейсов в Андроиде это скорее искусство, чем инженерная дисциплина.

UPD. Интересный комикс из commitstrip, иллюстрирующий похожий процесс, правда из веб-разработки:

Любое использование либо копирование материалов или подборки материалов сайта, элементов дизайна и оформления допускается лишь с разрешения правообладателя и только со ссылкой на источник: programador.ru

Телеграм канал: @prgrmdr
Почта для связи: vit [at] programmisty.com