Пользователь
0,0
рейтинг
14 декабря 2013 в 22:55

Разработка → Тайны кнопок в Android. Часть 2: Рефакторинг верстки tutorial

Приветствую, уважаемое сообщество.

В предыдущей статье мы, используя только верстку, сделали кнопку “включить/выключить”, и вот что у нас тогда получилось:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="6dp"
    android:background="#dddddd" >

    <ToggleButton
       android:id="@+id/act_main_btn_telephony"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:background="@drawable/button_background"
       android:drawableLeft="@drawable/icon_phone"
       android:drawableRight="@drawable/icon_on_off"
       android:gravity="left|center_vertical"
       android:textOn="Телефония"
       android:textOff="Телефония"
       android:textSize="24sp"
       android:textStyle="bold|italic"
       android:textColor="@color/text_color"
       android:onClick="onToggleButtonClick" />

</RelativeLayout>

XML-код получился довольно увесистый. В этой статье я покажу, как сделать его по-настоящему красивым. Мы сократим код вдвое, убрав все, что может затруднить сопровождение нашего приложения или вызвать недовольство тим-лида. В этой статье мы будем использовать строковые ресурсы (strings.xml), ресурсы размерностей (dimens.xml), стили и темы (styles.xml). Если вы не знакомы с первой частью статьи, рекомендую хотя бы пробежать ее глазами.

Вы также можете скачать проект Android-приложения из предыдущей статьи, который мы будем дорабатывать в этой части.

Выглядит наша кнопка вот так:
image
image

Справа изображены кнопки в нажатом состоянии, то есть пока мы удерживаем их пальцем. Слева — не нажатые, в режиме Включено и Выключено.

Текстовые ресурсы strings.xml

Займемся рефакторингом кода. Там точно есть недостатки, например слово Телефония. Чтобы приложение можно было перевести на несколько языков, все текстовые константы должны быть собраны в файле strings.xml. Он уже создан и находится в каталоге res/values. Откроем его и заменим его содержимое на следующее:
<?xml version="1.0" encoding="utf-8"?>
<resources>

    <string name="app_name">MysteriesOfButtons</string>
    <string name="action_settings">Настройки</string>
    <string name="act_main_telephony">Телефония</string>

</resources>

Так мы объявили текстовую константу act_main_telephony. Теперь заменим текст кнопки android:textOn и android:textOff на ссылку @string/act_main_telephony:
    <ToggleButton
       android:id="@+id/act_main_btn_telephony"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:background="@drawable/button_background"
       android:drawableLeft="@drawable/icon_phone"
       android:drawableRight="@drawable/icon_on_off"
       android:gravity="left|center_vertical"
       android:textOn="@string/act_main_telephony"
       android:textOff="@string/act_main_telephony"
       android:textSize="24sp"
       android:textStyle="bold|italic"
       android:textColor="@color/text_color" />

Немного лучше, хотя объем кода от этого только вырос, но это плата за возможность удобной локализации приложения. Как именно его можно локализовать? Наш файл strings.xml размещен в каталоге res/values. Сейчас мы пишем в него русскоязычный текст. Допустим, мы хотим поддерживать еще и английский язык. Для этого мы создаем каталог res/values-en, и в нем создаем еще один файл strings.xml. Он должен содержать все те же константы, что и первый файл, но уже на английском. При запуске приложения Android ищет в приложении в первую очередь ту локаль, которая установлена у пользователя в системе по умолчанию. Если в приложении нет ресурсов для этой локали, то Android берет ресурсы из локали по умолчанию, то есть из каталога res/values без суффиксов. Там может быть текст на любом языке, не обязательно русском или английском. Этот ресурс будет использован, если пользователю не подходит любой другой имеющийся в приложении ресурс. Локализовать можно не только ресурсы values, но и любые другие, например часто локализуют drawable, если какой-то текст задан картинкой. Если вас интересуют подробности, рекомендую почитать интересную статью о локализации приложений от Google и о том, как Android выбирает наиболее подходящий ресурс. Если будет время, я постараюсь подготовить отдельную статью о локализации с примерами, так как этот вопрос достаточно обширный.

Стили и темы: styles.xml

Что делать, если нам нужно десять таких кнопок, отличающихся только надписью и левой иконкой? Копировать все? Тогда размер файла макета будет непомерно большой и содержать 80% повторяющегося кода. Здесь нам на помощь приходят стили. Давайте вынесем в стили все, что только может повторяться. К счастью, плагин ADT для Eclipse несколько облегчает процесс, если вы не очень знакомы с разработкой файлов стиля Android.

Для извлечения стиля откроем наш макет activity_main.xml в режиме Graphical Layout и выделим кнопку. Щелкнем на ней правой кнопкой мыши и выберем пункт Extract Style:


Укажем имя стиля: styleOnOffButton и оставим отмеченными все атрибуты, кроме android:drawableLeft, android:textOn и android:textOff, которые будут отличаться у каждой кнопки в нашем приложении. Остальные атрибуты будут вынесены в стиль:


Нажмите ОК. Полученный стиль можно увидеть в файле res/values/styles.xml:
    <style name="styleOnOffButton">
       <item name="android:background">@drawable/button_background</item>
       <item name="android:drawableRight">@drawable/icon_on_off</item>
       <item name="android:gravity">left|center_vertical</item>
       <item name="android:onClick">onToggleButtonClick</item>
       <item name="android:textColor">@color/text_color</item>
       <item name="android:textSize">24sp</item>
       <item name="android:textStyle">bold|italic</item>
    </style>

А текст кнопки сильно уменьшился:
    <ToggleButton
       android:id="@+id/act_main_btn_telephony"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:drawableLeft="@drawable/icon_phone"
       android:textOn="@string/act_main_telephony"
       android:textOff="@string/act_main_telephony" />

Теперь, чтобы сделать десять кнопок, нужно писать куда меньше текста, и минимум дублирования кода. Обратите внимание: атрибуты android:layout_width и android:layout_height в стиль не выносятся. Они обязательно должны присутствовать у каждого элемента в файле макета. Атрибут android:id, конечно же, свой у каждого объекта, поэтому тоже не выносится в стиль. Тем не менее, полученный код достаточно компактен, чтобы его было удобно переиспользовать.

Если вы сейчас запустите приложение, то увидите, что все “поплыло”, и результат совсем не тот, что мы ожидали. Почему? Потому что стиль хоть и создался, но к кнопке не применился.

Применить стиль к кнопке, можно, указав ей атрибут style="@style/styleOnOffButton". Это было бы сделано автоматически, если бы мы включили опцию Set style attribute on extracted elements при извлечении стиля. Оба пути сработают, но это не лучшие варианты, так как эту строку нужно будет прописывать каждому экземпляру кнопки в каждом окне. Хотелось бы этого избежать. Как правило, в приложениях используется один стиль для одинаковых элементов управления, это одно из базовых правило дизайна. То ессть все кнопки типа включить/выключить выглядят одинаково, отличаясь только иконками и текстом. Редко кто делает одну прямоугольную, другую кругленькую, а третью ромбиком.

Это дает нам возможность прописать стиль в теме. Откройте файл styles.xml и найдите там следующий текст:
    <!-- Application theme. -->
    <style name="AppTheme" parent="AppBaseTheme">
       <!-- All customizations that are NOT specific to a particular API-level can go here. -->
    </style>

Это тема нашего приложения. Давайте укажем, что все кнопки ToggleButton должны иметь один и тот же стиль, наш стиль:
    <!-- Application theme. -->
    <style name="AppTheme" parent="AppBaseTheme">
       <!-- All customizations that are NOT specific to a particular API-level can go here. -->
       <item name="android:buttonStyleToggle">@style/styleOnOffButton</item>
    </style>

Что такое android:buttonStyleToggle, что еще можно стилизовать, и где об этом почитать? Я так и не смог найти полноценную документацию по стилям. Если кто-то ее видел, пожалуйста напишите в комментариях. Поэтому я обращаюсь напрямую к исходникам Android, к счастью они открыты для всех. Я работаю с ADT, и у меня исходники стилей Android лежат здесь: adt-bundle-windows\sdk\platforms\android-<Версия API>\data\res\values\themes.xml. Скачиваются исходники при помощи утилиты Android SDK Manager, которая запускается прямо из Eclipse, меню Window->Android SDK Manager.

И еще одно исправление мы должны сделать. Созданный нами стиль не наследуется от стиля кнопок, а это значит, что наш элемент управления потерял способность нажиматься. Это легко проверить, если сейчас запустить приложение и попробовать нажать кнопку. Как это вылечить? Нужно наследовать наш стиль от стиля android:style/Widget.Button.Toggle — это стиль всех ToggleButton по умолчанию:
    <style name="styleOnOffButton" parent="android:style/Widget.Button.Toggle">
       <item name="android:background">@drawable/button_background</item>
       <item name="android:drawableRight">@drawable/icon_on_off</item>
       <item name="android:gravity">left|center_vertical</item>
       <item name="android:onClick">onToggleButtonClick</item>
       <item name="android:textColor">@color/text_color</item>
       <item name="android:textSize">24sp</item>
       <item name="android:textStyle">bold|italic</item>
    </style>

Откуда я взял android:style/Widget.Button.Toggle? Из тех же исходников Android, файл styles.xml.

Ресурсы размерностей: dimens.xml

Теперь все работает, как надо. Но мы должны сделать еще пару небольших улучшений. Как вы заметили, в стиле styleOnOffButton есть атрибут android:textSize, который задан константой 24sp. Если в нашем приложении будет еще какой-то текст, кроме кнопок, вероятно мы захотим его сделать таким же по размеру для сохранения общего стиля. А это значит, что константу 24sp мы будем еще не раз использовать в разных местах. И если мы потом захотим поэкспериментировать с размером текста, то нам придется менять эти константы по всему приложению. Чтобы этого избежать, давайте объявим именованную константу размера. Откроем файл res/values/dimens.xml и заменим все его содержимое следующим:
<resources>
    <dimen name="text_size">24sp</dimen>
    <dimen name="activity_padding">6dp</dimen>
</resources>

Как видите, кроме text_size здесь есть еще одна константа — activity_padding. Если вспомнить код нашего макета, то там мы увидим константу android:padding="6dp" в теге RelativeLayout. А так как отступы от границ экрана у всех окон приложения также логично делать одинаковыми, это значение само-собой напрашивается в константы.

Теперь заменим константу 24sp в стиле на новый ресурс @dimen/text_size:
    <style name="styleOnOffButton" parent="android:style/Widget.Button.Toggle">
       <item name="android:background">@drawable/button_background</item>
       <item name="android:drawableRight">@drawable/icon_on_off</item>
       <item name="android:gravity">left|center_vertical</item>
       <item name="android:onClick">onToggleButtonClick</item>
       <item name="android:textColor">@color/text_color</item>
       <item name="android:textSize">@dimen/text_size</item>
       <item name="android:textStyle">bold|italic</item>
    </style>

И текст тега RelativeLayout в файле activity_main.xml с константой в ресурсах:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/activity_padding"
    android:background="#dddddd" >


И еще немного о стилях

Давайте еще раз подумаем о стилях. Мы сделали одинаковый стиль для всех ToggleButton, так почему бы не сделать стиль для всех окон? Тем более, что кроме отступов от границ у всех окон будет общий цвет фона. Такой стиль будет простой, нам не нужно его ни от чего наследовать, давайте пропишем его вручную. Откроем файл styles.xml и добавим в конец перед закрывающим тегом следующий код:
    <color name="activity_background_color">#dddddd</color>
   
    <style name="styleActivity">
       <item name="android:background">@color/activity_background_color</item>
       <item name="android:padding">@dimen/activity_padding</item>
    </style>

Вот еще один способ задавать цвет — в файле ресурсов с помощью тега color.

Не каждый RelativeLayout — главный элемент Activity, поэтому мы не можем прописать стиль всем RelativeLayout, как мы поступили с ToggleButton. В этом случае стиль нужно указывать явно в тех элементах, где это нужно, используя атрибут style. Давайте пропишем стиль нашему макету и посмотрим, что в и тоге у нас получилось:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    style="@style/styleActivity" >

    <ToggleButton
       android:id="@+id/act_main_btn_telephony"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:drawableLeft="@drawable/icon_phone"
       android:textOn="@string/act_main_telephony"
       android:textOff="@string/act_main_telephony" />

</RelativeLayout>

Все атрибуты окон у нас в стилях. Для каждой новой Activity достаточно указать стиль style="@style/styleActivity" ее корневому элементу, и окно будет выглядеть так же, как и другие окна приложения.

Заключение

Сравните исходный вариант макета с оптимизированным. Вот, что у нас было:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="6dp"
    android:background="#dddddd" >

    <ToggleButton
       android:id="@+id/act_main_btn_telephony"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:background="@drawable/button_background"
       android:drawableLeft="@drawable/icon_phone"
       android:drawableRight="@drawable/icon_on_off"
       android:gravity="left|center_vertical"
       android:textOn="Телефония"
       android:textOff="Телефония"
       android:textSize="24sp"
       android:textStyle="bold|italic"
       android:textColor="@color/text_color"
       android:onClick="onToggleButtonClick" />

</RelativeLayout>

И вот, что получилось:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    style="@style/styleActivity" >

    <ToggleButton
       android:id="@+id/act_main_btn_telephony"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       android:drawableLeft="@drawable/icon_phone"
       android:textOn="@string/act_main_telephony"
       android:textOff="@string/act_main_telephony" />

</RelativeLayout>

Правда стало красивее? Все общие атрибуты кнопок и окон в стилях. Создавая новые кнопки, мы будем прописывать им только те атрибуты, которые отличают новую кнопку от других: иконка, название, идентификатор, расположение в родительском элементе.

Все числовые константы спрятаны в файле dimens.xml, никакого копипаста одних и тех же констант у нас не будет. Все строковые константы — в файле strings.xml. Для локализации приложения под другие языки достаточно перевести только этот файл.

Разница между исходным и оптимизированным вариантом заметна даже на одном элементе окна. В реальных приложениях их в окне десятки, а потому оптимизированный код будет на порядок проще читать и сопровождать. Мое самое большое приложение iPUMB — ПУМБ online содержит около 40 activity. Если бы не стили, мне пришлось бы копировать оформление окон по 40 раз, и если дизайнеры придумают что-то новое, мне пришлось бы 40 раз этот код менять.

Еще одна прелесть стилей — это унификация кода при командной разработке. Один раз написанный файл стиля дал всем членам нашей команды разработчиков готовый инструмент для верстки окон, и все наши окна выглядели, как единое целое, и без каких-либо рефакторингов и «подгонок».

Буду рад, если эта статья оказалась вам полезна. В следующих статьях я обязательно поделюсь другими тонкостями верстки в Android.

Полезные ссылки

Готовый проект Android-приложения из данной статьи
Проект Android-приложения из предыдущей статьи, который мы дорабатываем в этой части
Тайны кнопок в Android. Часть 1: Основы верстки
Локализация приложений
Как Android выбирает наиболее подходящий ресурс
Степан Фурдей @DOCT0R
карма
12,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (12)

  • 0
    Замечательно и очень интересно. Спасибо.
    Вообще я это уже все освоил давно. Но так гладко написано, — что обе статьи прочитал :)
  • +5
    Все хорошо, но немного добавлю.

    1. Вдруг у меня в стилях нет AppTheme? Не пхохо бы написать, как и где этот стиль применяется. Что в AndroidManifest.xml, тег application и тд

    2. Зачем же корневому элементу в разметке меняете цвет, хотите поиметь проблем с overdraw?
    Цвет должен быть применен к окну, а не к элементу разметки, иначе, цвет будет отрисован 2 раза, первый раз, цвет окна, и второй раз цвет вашей активити. Или же надо убрать цвет у окна, но тогда точно удостоверится, что элемент с цветом покрывает весь экран.

    3. >> Созданный нами стиль не наследуется от стиля кнопок, а это значит, что наш элемент управления потерял способность нажиматься.
    Нет, это потому что атрибут android:clickable=false. Или потому что OnClickListener нет на кнопке.

    Вообще, имхо, лучше не наследоваться от 2.х стилей, а если уж делаете свой стиль, делать его полностью. Так вы можете быть уверенными, что выглядеть будет на всех устройствах одинаково. Потому что производитель устройства, мог посчитать нужным поменять что то в стиле, а вы, например, забыли этот атрибут переопределить, в итоге, на этом устройстве, будет выглядеть не так как задумано.

    4. >> Что такое android:buttonStyleToggle, что еще можно стилизовать, и где об этом почитать?
    Это атрибут, что бы абстрагироваться от конкретного стиля. Почти у каждого View есть свой. Можно пробовать смотреть в документацию к R.attr и там их искать, или проще открыть исходник того же ToggleButton и там будет имя атрибута стиля, а так же все другие атрибуты специфичные конкретно для этого элемента.

    5. >> Если бы не стили, мне пришлось бы копировать оформление окон по 40 раз, и если дизайнеры придумают что-то новое, мне пришлось бы 40 раз этот код менять.
    Если оформление вообще одно и тоже, то можно проще. Делаете одну активити с базовым оформление, и все остальные наследуете от нее, и уже добавляете только разметку с содержимым. Но это при условии что вам нужно стилизовать именно базовую разметку, отступы для контента и тд.
    Если это стилизация окна, которая может быть сделана через стили. То тогда, соответственно, ничего этого вообще не нужно, а нужен только отдельный стиль для активити, с цветом для фона, actionbar и тд.
    • 0
      Спасибо за Ваш комментарий, обязательно учту при доработке статьи
  • –2
    У меня одного в стилях лежат удобные заготовки под названием «fill_fill», «fill_wrap», «wrap_fill», «wrap_wrap»?
    • 0
      кажется да.
      На мой взгляд это не совсем удобно и не гибко, если я Вас правильно понял.
      • 0
        На самом деле это очень удобно. Естественно файл со стилями не ограничивается только этими четырьмя, но именно эти используются чаще всего. К примеру для ващего случая:

        Для стиля styleActivity был бы назначен parent=«fill_fill»

        и разметка RelativeLayout сократилась бы до 1 строчки. И если бы надо было поменять к примеру android:layout_height=«match_parent» на android:layout_height=«wrap_content» — мы бы просто поменяли на «fill_wrap».

        • 0
          Безусловно, однако представьте большое приложение, где один стиль может быть применен на компоненты c ралзличными layout_width и layout_height.

          Если попытаться применить Ваш подход, то появится дублирование кода. Если сделать исключение, то код будет не однородным. Все это может привести к путанице.

          • 0
            «где один стиль может быть применен на компоненты c ралзличными layout_width и layout_height.» — тогда этот стиль не должен определять layout_width и layout_height. (соответсвенно не надо наследовать от указаных стилей). Но таких случаев единицы.

            Если применять мой подход, то как раз мы уйдем от разростания кода, тк каждый View должен определять layout_width и layout_height в обязательном порядке. И добиваемся сокращения:

            Было:

            android:layout_width=«match_parent»
            android:layout_height=«wrap_content»

            Стало:

            style="@ style/wrap_wrap"

            Если вы до сих пор не увидели профит — я не знаю, чем вам больше помочь.

            • 0
              К сожалению, до сих пор не увидел, но все же, спасибо за старания.
  • 0
    Спасибо :)
    • 0
      Чувак, долго ты добирался до статьи… :)
      • 0
        Начинать никогда не поздно.

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