- PVSM.RU - https://www.pvsm.ru -

Верстка Android макетов без боли

Разрабатывать интерфейс Android приложений — непростая задача. Приходится учитывать разнообразие разрешений [1] и плотностей пикселей (DPI). Под катом практические советы о верстке макетов дизайна Android приложений в Layout, который совпадает с макетом на одном устройстве а на остальных растягивается без явных нарушений дизайна: выхода шрифтов за границы; огромных пустых мест и других артефактов.

Верстка Android макетов без боли

На iPhone layout задаются абсолютно и всего под два экрана iPhone 4 и iPhone 5. Рисуем два макета, пишем приложение и накладываем полупрозрачные скриншоты на макеты. Проблем нет, воля дизайнера ясна, проверить что она исполнена может сам разработчик, тестировщик или, даже, билд-сервер.

Под Android у нас две проблемы: нельзя нарисовать бесконечное число макетов и нельзя сверить бесконечное число устройств с конечным числом макетов. Дизайнеры проверяют вручную. Разработчики же часто понятия не имеют как правильно растягивать элементы и масштабировать шрифты. Количество итераций стремится к бесконечности.

Чтобы упорядочить хаос мы пришли к следующему алгоритму верстки. Макеты рисуются и верстаются под любой флагманский full-hd телефон. На остальных красиво адаптируются. Готовое приложение проверяет дизайнер на популярных моделях смартфонов. Метод работает для всех телефонов, для планшетов (>6.5 дюймов) требуются отдельные макеты и верстка.

Под рукой у меня только Nexus 4 возьмем его характеристики экрана для примера.

Макеты ненастоящего приложения-портфолио которые будем верстать (полноразмерные по клику).
Верстка Android макетов без боли [2]Верстка Android макетов без боли [3]Верстка Android макетов без боли [4]

Layout

Основную верстку делаем через вложенные LinearLayout. Размеры элементов и блоков в пикселях переносим с макета в weight и weightSum [5] соответственно. Отступы верстаем FrameLayout или в нужных местах добавляем Gravity [6].

Для примера сверстаем ячейку списка приложений:
Верстка Android макетов без боли

<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 — ширина. Эти значения используются, чтобы выяснить во сколько раз высота и ширина устройства, на котором запущено приложение, отличаются от эталонного.

ScrollView и List

Подход с 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, чтобы растягиваться пропорционально контенту.

Верстка Android макетов без боли

Shape

Если ресурс легко раскладывается на простые геометрические фигуры и градиенты лучше вместо нарезки использовать xml-shape [8]. Для примера нарисуем фон рамку вокруг проекта в списке, которую мы выше нарезали как Nine-patch.

Верстка Android макетов без боли

<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? Сколько макетов рисует дизайнер? Как обращаетесь с графическими ресурсами?


Подписывайтесь на наш хабра-блог (кнопка справа вверху). Каждый четверг интересные статьи о мобильной разработке, маркетинге и бизнесе мобильной студии. Следующая статья (5 сентября) «C# async на iOS и 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/