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

в 12:07, , рубрики: android, dpi, mobile development, pixel perfect, Блог компании Touch Instinct, боль, мобильная верстка, Разработка под android, метки: , , , ,

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

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

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

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

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

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

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

Layout

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

Для примера сверстаем ячейку списка приложений:
Верстка 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 везде где возможно, где невозможно — перерисуйте дизайн.
2. Простые элементы рисуйте с помощью Shape
3. Избегайте масштабирования изображений в runtime

Nine-patch это графический ресурс содержащий в себе мета-информацию о том как он должен растягиваться. Подробнее в документации Android или на Хабре.

Nine-patch нужно нарезать под все dpi: ldpi mdpi tvdpi hdpi, xhdpi, xxhdpi. Растягивание ресурсов во время работы приложения это плохо, а растягивание Nine-Patch приводит к неожиданным артефактам. Ни в коем случае не задавайте в Nine-patch отступы, они оформляются отдельными элементами layout, чтобы растягиваться пропорционально контенту.

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

Shape

Если ресурс легко раскладывается на простые геометрические фигуры и градиенты лучше вместо нарезки использовать xml-shape. Для примера нарисуем фон рамку вокруг проекта в списке, которую мы выше нарезали как 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 байта на пиксель), при масштабировании создается еще один bitmap, скажем в 500кб. Или 1,5мб из доступных 24мб на процесс. Мы не раз сталкивались с нехваткой памяти в богатых на графику проектах.

Поэтому картинки которые нельзя описать ни Nine-patch ни Shape я предлагаю поставлять в приложении как огромный ресурс в папке nodpi и при первом запуске масштабировать изображение до нужного размера и кешировать результат. Это позволит нам ускорить работу приложения (не считая первого запуска) и уменьшить потребление памяти.

Для сложных ресурсов подгружаемых с сервера (иконки приложений на наших макетах) идеальный вариант если сервер будет отдавать картинки любого размера. Как, например, сделано на проекте Stream. Приложение просчитывает нужный размер картинки для экрана смартфона, где запущено, и запрашивает их у сервера.

http://<secret_domain>/media/img/movies/vposter/plain/22741680/<любая ширина px>_<любая высота px>.jpg

P.S. советы придуманы и основа поста написаны нашим Android-гуру Лешей, огромное ему спасибо!

А как вы рекомендуете верстать макеты под Android? Сколько макетов рисует дизайнер? Как обращаетесь с графическими ресурсами?


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

Автор: junk

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js