- PVSM.RU - https://www.pvsm.ru -
Разрабатывать интерфейс Android приложений — непростая задача. Приходится учитывать разнообразие разрешений [1] и плотностей пикселей (DPI). Под катом практические советы о верстке макетов дизайна Android приложений в Layout, который совпадает с макетом на одном устройстве а на остальных растягивается без явных нарушений дизайна: выхода шрифтов за границы; огромных пустых мест и других артефактов.
На iPhone layout задаются абсолютно и всего под два экрана iPhone 4 и iPhone 5. Рисуем два макета, пишем приложение и накладываем полупрозрачные скриншоты на макеты. Проблем нет, воля дизайнера ясна, проверить что она исполнена может сам разработчик, тестировщик или, даже, билд-сервер.
Под Android у нас две проблемы: нельзя нарисовать бесконечное число макетов и нельзя сверить бесконечное число устройств с конечным числом макетов. Дизайнеры проверяют вручную. Разработчики же часто понятия не имеют как правильно растягивать элементы и масштабировать шрифты. Количество итераций стремится к бесконечности.
Чтобы упорядочить хаос мы пришли к следующему алгоритму верстки. Макеты рисуются и верстаются под любой флагманский full-hd телефон. На остальных красиво адаптируются. Готовое приложение проверяет дизайнер на популярных моделях смартфонов. Метод работает для всех телефонов, для планшетов (>6.5 дюймов) требуются отдельные макеты и верстка.
Под рукой у меня только Nexus 4 возьмем его характеристики экрана для примера.
Макеты ненастоящего приложения-портфолио которые будем верстать (полноразмерные по клику).
[2] [3] [4]
Основную верстку делаем через вложенные LinearLayout. Размеры элементов и блоков в пикселях переносим с макета в weight и weightSum [5] соответственно. Отступы верстаем FrameLayout или в нужных местах добавляем Gravity [6].
Для примера сверстаем ячейку списка приложений:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 488 = 768 - 40 (левый отступ) - 40 (правый отступ) - 200 (ширина картинки) -->
<LinearLayout
android:id="@+id/appLstItemLayout"
android:orientation="horizontal"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
android:weightSum="488"
android:background="@drawable/bg_item">
<ImageView
android:id="@+id/appImg"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:src="@drawable/square"/>
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="20"/>
<!-- 130 = высота ячейки - 40 (высота звездочек) -->
<LinearLayout
android:orientation="vertical"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="428"
android:gravity="center"
android:weightSum="130">
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="55"/>
<TextView
android:id="@+id/titleTxt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom"/>
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="10"/>
<ru.touchin.MySimpleAndAwesomeRatingBar
android:id="@+id/appRatingBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"/>
<FrameLayout
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="25"/>
</LinearLayout>
</LinearLayout>
</FrameLayout>
Дальше нам потребуется много DisplayMetrics-магии, напишем для него static helper.
public class S {
private static final int ORIGINAL_VIEW_WIDTH = 768;
private static final int ORIGINAL_VIEW_HEIGHT = 1184;
private static final int ORIGINAL_VIEW_DIAGONAL = calcDiagonal(ORIGINAL_VIEW_WIDTH, ORIGINAL_VIEW_HEIGHT);
private static int mWidth;
private static int mHeight;
private static int mDiagonal;
private static float mDensity;
static {
DisplayMetrics metrics = TouchinApp.getContext().getResources().getDisplayMetrics();
mWidth = metrics.widthPixels;
mHeight = metrics.heightPixels;
mDiagonal = calcDiagonal(mWidth, mHeight);
mDensity = metrics.density;
}
public static int hScale(int value){
return (int)Math.round(value * mWidth / (float) ORIGINAL_VIEW_WIDTH);
}
public static int vScale(int value){
return (int)Math.round(value * mHeight / (float) ORIGINAL_VIEW_HEIGHT);
}
public static int dScale(int value){
return (int)Math.round(value * mDiagonal / (float) ORIGINAL_VIEW_DIAGONAL);
}
public static int pxFromDp(int dp){
return (int)Math.round(dp * mDensity);
}
private static int calcDiagonal(int width, int height){
return (int)Math.round(Math.sqrt(width * width + height * height));
}
}
1184 это высота Nexus 4 без кнопок, 768 — ширина. Эти значения используются, чтобы выяснить во сколько раз высота и ширина устройства, на котором запущено приложение, отличаются от эталонного.
Подход с weightSum не примемим к прокручивающимся элементам, их внутренний размер вдоль прокрутки ничем не ограничен. Для верстки ScrollView и List нам потребуется задать их размеры в коде (130 — высота элемента списка).
if (view == null) {
view = mInflater.inflate(R.layout.item_app_list, viewGroup, false);
view.setLayoutParams(new AbsListView.LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT, S.dScale(130)));
}
И дальше можно применять трюк с weightSum.
Размер иконок приложений задается в коде:
view.findViewById(R.id.appImg).setLayoutParams(new LinearLayout.LayoutParams(S.dScale(240) - S.pxFromDp(20), S.dScale(240) - S.pxFromDp(20)));
Где 240 высота элемента списка, 20 высота отступа сверху и снизу.
Андроид не предоставляет единицу измерения пропорциональную размеру экрана. Размеры шрифтов рассчитываем на основании диагонали устройства:
textSizePx = originalTextSizePx * (deviceDiagonalPx / originalDeviceDiagonalPx )
Да, размеры шрифта придется задавать в коде (36 размер шрифта в пикселях на оригинальном макете).
titleTxt.setTextSize(TypedValue.COMPLEX_UNIT_PX, S.dScale(36));
1. Используйте Nine-patch [7] везде где возможно, где невозможно — перерисуйте дизайн.
2. Простые элементы рисуйте с помощью Shape [8]
3. Избегайте масштабирования изображений в runtime
Nine-patch это графический ресурс содержащий в себе мета-информацию о том как он должен растягиваться. Подробнее в документации Android [7] или на Хабре [9].
Nine-patch нужно нарезать под все dpi: ldpi mdpi tvdpi hdpi, xhdpi, xxhdpi. Растягивание ресурсов во время работы приложения это плохо, а растягивание Nine-Patch приводит к неожиданным артефактам. Ни в коем случае не задавайте в Nine-patch отступы, они оформляются отдельными элементами layout, чтобы растягиваться пропорционально контенту.
Если ресурс легко раскладывается на простые геометрические фигуры и градиенты лучше вместо нарезки использовать xml-shape [8]. Для примера нарисуем фон рамку вокруг проекта в списке, которую мы выше нарезали как Nine-patch.
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<!-- "shadow" -->
<item>
<shape android:shape="rectangle" >
<corners android:radius="5px" />
<solid android:color="#08000000"/>
</shape>
</item>
<item
android:bottom="1px"
android:right="1px"
android:left="1px"
android:top="1px">
<shape android:shape="rectangle" >
<corners android:radius="4px" />
<solid android:color="#10000000"/>
</shape>
</item>
<item
android:bottom="2px"
android:right="2px"
android:left="2px"
android:top="2px">
<shape android:shape="rectangle" >
<corners android:radius="3px" />
<solid android:color="#10000000"/>
</shape>
</item>
<item
android:bottom="3px"
android:right="3px"
android:left="3px"
android:top="3px">
<shape android:shape="rectangle">
<corners android:radius="2px" />
<solid android:color="#ffffff"/>
</shape>
</item>
</layer-list>
Масштабирование графики силами Android трудоемкая и затратная по памяти операция. Картинки внутри Android обрабатываются как bitmap. Например, наш логотип в размере 500x500 со сплешскрина распакуется в bitmap размером 1мб (4 байта на пиксель [10]), при масштабировании создается еще один bitmap, скажем в 500кб. Или 1,5мб из доступных 24мб на процесс [11]. Мы не раз сталкивались с нехваткой памяти в богатых на графику проектах.
Поэтому картинки которые нельзя описать ни Nine-patch ни Shape я предлагаю поставлять в приложении как огромный ресурс в папке nodpi и при первом запуске масштабировать изображение до нужного размера и кешировать результат. Это позволит нам ускорить работу приложения (не считая первого запуска) и уменьшить потребление памяти.
Для сложных ресурсов подгружаемых с сервера (иконки приложений на наших макетах) идеальный вариант если сервер будет отдавать картинки любого размера. Как, например, сделано на проекте Stream [12]. Приложение просчитывает нужный размер картинки для экрана смартфона, где запущено, и запрашивает их у сервера.
http://<secret_domain>/media/img/movies/vposter/plain/22741680/<любая ширина px>_<любая высота px>.jpg
P.S. советы придуманы и основа поста написаны нашим Android-гуру Лешей, огромное ему спасибо!
А как вы рекомендуете верстать макеты под Android? Сколько макетов рисует дизайнер? Как обращаетесь с графическими ресурсами?
Автор: junk
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android/42072
Ссылки в тексте:
[1] разнообразие разрешений: http://opensignal.com/reports/fragmentation.php
[2] Image: http://habrastorage.org/storage3/cd4/4c8/586/cd44c85864d05473ac9d0eef6052fc41.jpg
[3] Image: http://habrastorage.org/storage3/f6f/82b/9fd/f6f82b9fdc3c5ab0c62d57e875c47e05.jpg
[4] Image: http://habrastorage.org/storage3/6a4/e45/c66/6a4e45c665faced0eea97d830f90f0a2.jpg
[5] weightSum: http://developer.android.com/reference/android/widget/LinearLayout.html#attr_android:weightSum
[6] Gravity: http://developer.android.com/reference/android/view/Gravity.html
[7] Nine-patch: http://developer.android.com/guide/topics/graphics/2d-graphics.html#nine-patch
[8] Shape: http://developer.android.com/guide/topics/resources/drawable-resource.html#Shape
[9] на Хабре: http://habrahabr.ru/post/113623/
[10] 4 байта на пиксель: http://developer.android.com/reference/android/graphics/Bitmap.Config.html
[11] 24мб на процесс: http://stackoverflow.com/questions/8903340/does-android-application-memory-limit-apply-to-entire-app-or-per-process-of-app
[12] Stream: http://touchin.ru/portfolio/omlet?utm_campaign=73
[13] Источник: http://habrahabr.ru/post/191910/
Нажмите здесь для печати.