Глава 8.Макеты и виджеты «ритм 48dp». Полный список рекомендаций находится по адресу https://developer. android.com/design/index.html Современные приложения Android должны соответствовать этим рекоменда- циям, насколько это возможно. Рекомендации в значительной мере зависят от новой функциональности Android SDK, которая не всегда доступна или легко реализуема на старых устройствах. Некоторые рекомендации могут соблюдаться с использованием библиотеки поддержки, но для многих требуются библиотеки сторонних разработчиков — такие, как ActionBarSherlock , о которой можно про- читать в главе 16. Параметры макета Вероятно, вы уже заметили, что некоторые имена атрибутов начинаются с layout_ ( android:layout_marginLeft ), а у других атрибутов этого префикса нет ( android:text ). Атрибуты, имена которых не начинаются с layout_ , являются рекомендациями для виджетов. При заполнении виджет вызывает метод для настройки своей конфигу- рации на основании этих атрибутов и их значений. Если имя атрибута начинается с layout_ , то этот атрибут является директивой для родителя этого виджета. Такие атрибуты, называемые параметрами макета, сообщают родительскому макету, как следует расположить дочерний элемент внутри родителя. Даже если объект макета (например, LinearLayout ) является корневым элементом, он все равно остается виджетом, имеющим родителя, и у него есть параметры макета. Определяя элемент LinearLayout в файле fragment_crime.xml , мы задали атрибуты android:layout_width и android:layout_height . Эти атрибуты будут использоваться родительским макетом LinearLayout при заполнении. В данном случае параметры макета LinearLayout будут использоваться элементом FrameLayout в представлении содержимого CrimeActivity Поля и отступы В файле fragment_crime.xml виджетам назначаются атрибуты margin и padding . Начи- нающие разработчики иногда путают эти атрибуты, определяющие соответственно поля и отступы. Теперь, когда вы понимаете, что такое параметр макета, будет проще объяснить, чем они различаются. Атрибуты margin являются параметрами макета; они определяют расстояние между виджетами. Так как виджет располагает информацией только о самом себе, за соблюдение полей отвечает родитель виджета. Напротив, отступ не является параметром макета. Атрибут android:padding сообща- ет виджету, с каким превышением размера содержимого он должен прорисовывать себя. Допустим, вы хотите заметно увеличить размеры кнопки даты без изменения размера текста. Добавьте в Button следующий атрибут, сохраните макет и запустите приложение заново.
172 Глава 8. Макеты и виджеты Рис. 8.4. CrimeFragment в альбомной ориентации Кнопка даты становится слишком длинной; было бы лучше, если бы в альбомной ориентации кнопка и флажок располагались рядом друг с другом. Для этого создайте каталог res/layout-land (щелкните правой кнопкой мыши на каталоге res/ на панели Package Explorer и выберите команду New Folder ), после чего скопируйте файл frag- ment_crime.xml в res/layout-land/ Чтобы внести изменения в графическом конструкторе, сначала закройте файл res/ layout/fragment_crime.xml , если он открыт в редакторе. Откройте файл res/layout-land/ fragment_crime.xml и выберите вкладку Graphical Layout В середине графического конструктора макета в области предварительного про- смотра отображается уже знакомый альбомный макет. Слева находится палитра. Панель содержит все необходимые виджеты, упорядоченные по категориям. Предва- ритель- ный про- смотр Свойства Структура Палитра Рис. 8.5. Графический конструктор макета
Использование графического конструктора 173 Справа от области предварительного просмотра находится панель структуры. На ней показана иерархия виджетов в макете. Под панелью структуры находится панель свойств. На ней можно просматривать и редактировать атрибуты виджета, выделенного на панели структуры. Какие же изменения следует внести в этот макет? Взгляните на рис. 8.6. Рис. 8.6. Альбомный макет для CrimeFragment Изменения можно разделить на четыре группы: Добавление виджета LinearLayout в макет. Редактирование атрибутов LinearLayout Назначение виджетов Button и CheckBox потомками LinearLayout Обновление параметров макета Button и CheckBox Добавление нового виджета Чтобы добавить виджет, можно выделить его в палитре и перетащить на панель структуры. Щелкните на категории Layouts в палитре. Выберите пункт LinearLayout (Horizontal) и перетащите на панель структуры. Отпустите перетаскиваемый объект LinearLayout на коренном элементе LinearLayout , чтобы сделать его прямым по- томком корневого элемента LinearLayout Виджеты также можно добавлять перетаскиванием из палитры в область предва- рительного просмотра. Однако виджеты Layout часто пусты или закрыты другими представлениями, поэтому может быть трудно понять, где следует разместить виджет в области предварительного просмотра для получения нужной иерархии.
174 Глава 8. Макеты и виджеты Перетаскивание на панель структуры существенно упрощает выполнение этой операции. Рис. 8.7. Добавление виджета LinearLayout в файл fragment_crime.xml Редактирование атрибутов в свойствах Выберите новый виджет LinearLayout на панели структуры, чтобы отобразить его атрибуты на панели свойств. Разверните категорию Layout Parameters , затем катего- рию Margins Мы хотим, чтобы поля нового виджета Linear- Layout совпадали с полями других виджетов. Выделите поле рядом с Left и введите значение 16dp . Сделайте то же самое для правого ( Right ) поля. (На некоторых платформах при попытке ввести данные в эти поля на экране появляется вре- менное окно. Это обходное решение известной ошибки… которое, к сожалению, не позволяет ввести значение. Если это произойдет, сохра- ните файл, переключитесь на разметку XML и скопируйте два атрибута margin из EditText в LinearLayout ). Сохраните файл макета и переключитесь на раз- метку XML при помощи вкладки fragment_crime. xml в нижней части области предварительного просмотра. Вы увидите только что добавленный элемент LinearLayout с атрибу- тами полей. Рис. 8.8. Назначение полей на панели свойств
Реорганизация виджетов на панели структуры 175 Реорганизация виджетов на панели структуры Следующий шаг — назначение виджетов Button и CheckBox потомками нового виджета LinearLayout . Вернитесь к графическому конструктору, выберите на па- нели структуры виджет Button и перетащите его на LinearLayout На панели структуры отражается тот факт, что Button теперь является потомком нового вид- жета LinearLayout . Проделайте то же самое с CheckBox Если потомки размещаются не в том порядке, их можно переупорядочить посредством пере- таскивания. Также на панели структуры можно удалять виджеты из макета, но будьте осторож- ны: удаление виджета приводит к удалению его потомков. В области предварительного просмотра виджет CheckBox не виден — его скрывает Button . Вид- жет LinearLayout проверил ширину ( match_par- ent ) своего первого потомка ( Button ) и выделил ему все пространство, ничего не оставив Check- Box (рис. 8.10). Рис. 8.10. Виджет Button, который был определен первым, скрывает CheckBox Рис. 8.9. Виджеты Button и CheckBox теперь являются потомками LinearLayout
176 Глава 8. Макеты и виджеты Чтобы восстановить равноправие потомков LinearLayout , мы изменим параметры макета потомков. Обновление параметров макета потомков Сначала выделите кнопку даты на панели структуры. На панели свойств щелкни- те на текущем значении Width и замените его значением wrap_content Удалите оба значения 16dp полей кнопки. Теперь, когда кнопка находится внутри LinearLayout , поля ей не нужны. Найдите поле Weight в разделе Layout Parameters и задайте ему значение 1. Это поле соответству- ет атрибуту android:layout_weight на рис. 8.6. Выберите виджет CheckBox на панели структуры и внесите те же изменения: атрибут Width дол- жен содержать wrap_content , атрибут Weight — 1, а атрибуты полей должны быть пустыми. В области предварительного просмотра убе- дитесь в том, что оба виджета теперь видны. Сохраните файл и вернитесь к XML, чтобы под- твердить изменения. В листинге 8.9 приведена соответствующая разметка XML. Листинг 8.9. Разметка XML макета, созданного в графическом конструкторе (layout-land/fragment_crime.xml) android:layout_height="wrap_content" android:text="@string/crime_details_label" /> android:layout_height="wrap_content" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" >
Рис. 8.11. Ширине кнопки задается значение wrap_content
Реорганизация виджетов на панели структуры 177 Как работает android:layout_weight Атрибут веса android:layout_weight сообщает виджету LinearLayout , как он должен распределить потомков по размеру контейнера. Обоим виджетам заданы одина- ковые значения ширины, но это не гарантирует, что они будут иметь одинаковую ширину на экране. Для определения ширин дочерних представлений LinearLayout использует комбинацию параметров layout_width и layout_weight LinearLayout вычисляет ширину представления в два прохода. На первом проходе LinearLayout проверяет значение layout_width (или layout_height для вертикаль- ной ориентации). Значение layout_width как для Button , так и для CheckBox теперь равно wrap_content , так что каждому представлению выделяется место, достаточное только для его прорисовки (рис. 8.12). (По области предварительного просмотра трудно понять, как работает систе- ма весов, потому что содержимое кнопки в макет не входит. На следующих рисунках показано, как будет выглядеть LinearLayout , если кнопка уже имеет содержимое.) Рис. 8.12. Проход 1: распределение пространства на основании layout_width На следующем проходе LinearLayout распределяет дополнительное пространство на основании значений layout_weight (рис. 8.13). Рис. 8.13. Проход 2: распределение дополнительного пространства на основании layout_weight В нашем макете Button и CheckBox имеют одинаковые значения layout_weight , по- этому дополнительное пространство распределяется в соотношении 50/50. Если задать весовой коэффициент Button равным 2, то ей будет выделено 2/3 дополни- тельного пространства, а CheckBox достанется всего 1/3 (рис. 8.14). Рис. 8.14. Неравномерное распределение дополнительного пространства с пропорцией layout_weight 2:1 В качестве весового коэффициента может использоваться любое вещественное число. Программисты используют разные системы обозначений весов. В файле
178 Глава 8. Макеты и виджеты fragment_crime.xml используется система «рецепт коктейля». Также часто применя- ются наборы весов, сумма которых составляет 1.0 или 100; в этом случае вес кнопки в приведенном примере составит 0.66 или 66 соответственно. Что если вы хотите, чтобы виджет LinearLayout выделял для каждого представле- ния ровно 50 % своей ширины? Просто пропустите первый проход, задав атрибуту layout_width каждого виджета значение 0dp вместо wrap_content . В этом случае LinearLayout принимает решения только на основании значений layout_weight (рис. 8.15). Рис. 8.15. При layout_width="0dp" учитываются только значения layout_weight Графический конструктор макетов Графический конструктор макетов удобен, и Android совершенствует его с каждым выпуском ADT. Однако порой он работает медленно и ненадежно и тогда про- ще использовать прямой ввод разметки XML. Вы можете переключаться между внесением изменений в графическом конструкторе и в XML (для надежности не забудьте сохранить файл перед переключением). Не стесняйтесь использовать графический конструктор для создания макетов из этой книги. В дальнейшем, когда потребуется создать макет, мы будем приводить диаграмму наподобие рис. 8.6. Вы сами сможете решить, как создать ее — в виде разметки XML, в графическом конструкторе или сочетанием этих двух способов. Идентификаторы виджетов и множественные макеты Два макета, созданные для CriminalIntent, отличаются незначительно, но в неко- торых ситуациях возможны более серьезные расхождения. В таких случаях перед обращением к виджету в коде необходимо убедиться в том, что он действительно существует. Если виджет присутствует в одном макете и отсутствует в другом, то для проверки его наличия в текущей ориентации перед вызовом методов следует использовать проверку null : Button landscapeOnlyButton = (Button)v.findViewById(R.id.landscapeOnlyButton); if (landscapeOnlyButton != null) { // Операции } Наконец, помните, что для того чтобы ваш код мог найти виджет, последний дол- жен иметь одинаковые значения атрибута android:id во всех макетах, в которых он присутствует.
Упражнение. Форматирование даты 179 Упражнение. Форматирование даты Объект Date больше напоминает временную метку (timestamp), чем традиционную дату. При вызове toString() для Date вы получаете именно временную метку, ко- торая отображается на кнопке. Временные метки хорошо подходят для отчетов, но на кнопке было бы лучше выводить дату в формате, более привычном для людей (например, «Oct 12, 2012»). Для этого можно воспользоваться экземпляром класса android.text.format.DateFormat . Хорошей отправной точкой в работе станет опи- сание этого класса в документации Android. Используйте методы класса DateFormat для формирования строки в стандартном формате или же подготовьте собственную форматную строку. Чтобы задача стала более творческой, попробуйте создать форматную строку для вывода дня неде- ли («Tuesday, Oct 12, 2012»).
Вывод списков и ListFragment Уровень модели CriminalIntent в настоящее время состоит из единственного эк- земпляра Crime . В этой главе мы обновим приложение CriminalIntent, чтобы оно поддерживало списки. В списке для каждого преступления будет отображаться краткое описание и дата, а также признак его раскрытия. Рис. 9.1. Список преступлений 9
Обновление уровня модели CriminalIntent 181 На рис. 9.2 показана общая структура приложения CriminalIntent для этой главы. Представление Модель Контроллер Рис. 9.2. Приложение CriminalIntent со списком На уровне модели появляется новый объект CrimeLab , который представляет собой централизованное хранилище для объектов Crime Для отображения списка на уровне контроллера CriminalIntent появляется новая активность и новый фрагмент: CrimeListActivity и CrimeListFragment CrimeListFragment субклассирует ListFragment — субкласс Fragment , который обладает встроенными средствами для поддержки списков. Эти контроллеры вза- имодействуют друг с другом и CrimeLab для обращения к данным уровня модели. (Где находятся классы CrimeActivity и CrimeFragment на рис. 9.2? Они являются частью представления детализации, поэтому на рисунке их нет. В главе 10 мы свя- жем части списка и детализации CriminalIntent.) На рис. 9.2 также видны объекты представлений, связанные с CrimeListActivity и CrimeListFragment . Представление активности состоит из объекта FrameLayout , содержащего фрагмент. Представление фрагмента состоит из ListView . Взаимо- действие между ListFragment и ListView более подробно рассматривается позднее в этой главе. Обновление уровня модели CriminalIntent Прежде всего необходимо преобразовать уровень модели CriminalIntent из одного объекта Crime в массив объектов Crime ArrayList — класс Java, реализующий упорядоченный список объектов заданного типа. В нем содержатся методы извлечения, добавления и удаления элементов.
182 Глава 9. Вывод списков и ListFragment Синглеты и централизованное хранение данных Для хранения массива-списка преступлений будет использоваться синглетный (singleton) класс. Такие классы допускают создание только одного экземпляра. Экземпляр синглетного класса существует до тех пор, пока приложение остается в па- мяти, так что при хранении списка в синглетном объекте данные остаются доступными, что бы ни происходило с активностями, фрагментами и их жизненными циклами. Чтобы создать синглетный класс, следует создать класс с закрытым конструктором и методом get() , который возвращает экземпляр. Если экземпляр уже существует, то get() просто возвращает его. Если экземпляр еще не существует, то get() вы- зывает конструктор для его создания. Щелкните правой кнопкой мыши на пакете com.bignerdranch.android.criminalintent и вы- берите команду New Class . Введите имя класса CrimeLab и щелкните на кнопке Finish В файле CrimeLab.java реализуйте CrimeLab как синглетный класс с закрытым кон- структором и методом get(Context) Листинг 9.1. Синглетный класс (CrimeLab.java) public class CrimeLab { private static CrimeLab sCrimeLab; private Context mAppContext; private CrimeLab(Context appContext) { mAppContext = appContext; } public static CrimeLab get(Context c) { if (sCrimeLab == null) { sCrimeLab = new CrimeLab(c.getApplicationContext()); } return sCrimeLab; } } Обратите внимание на префикс s у переменной sCrimeLab . Мы используем это ус- ловное обозначение Android, чтобы показать, что переменная sCrimeLab является статической. Конструктору CrimeLab передается параметр Context . В Android такая ситуация встречается очень часто; наличие параметра Context позволяет синглетному классу запускать активности, обращаться к ресурсам проекта, находить закрытое храни- лище вашего приложения и т. д. Обратите внимание: в get(Context) параметр Context не передается конструктору. В Context может содержаться Activity или другой объект Context — например, Service . Мы не можем быть уверены в том, что произвольный объект Context бу- дет существовать все то время, когда он может понадобиться CrimeLab — то есть на протяжении всего жизненного цикла приложения. Чтобы гарантировать, что синглетному классу для работы будет доступен объект Context с долгим сроком жизни, мы вызываем getApplicationContext() и подменяем
Синглеты и централизованное хранение данных 183 переданный объект Context контекстом приложения. Контекст приложения глоба- лен по отношению к вашему приложению. Если в вашем приложении задействован синглетный класс уровня приложения, всегда используйте контекст приложения. Для начала предоставим CrimeLab несколько объектов Crime для хранения. В кон- структоре CrimeLab создайте пустой массив ArrayList объектов Crime . Также добавьте два метода: getCrimes() возвращает список, а getCrime(UUID) возвращает объект Crime с заданным идентификатором (листинг 9.2). Листинг 9.2. Создание списка ArrayList объектов Crime (CrimeLab.java) public class CrimeLab { private ArrayList mCrimes; private static CrimeLab sCrimeLab; private Context mAppContext; private CrimeLab(Context appContext) { mAppContext = appContext; mCrimes = new ArrayList(); } public static CrimeLab get(Context c) { } public ArrayList getCrimes() { return mCrimes; } public Crime getCrime(UUID id) { for (Crime c : mCrimes) { if (c.getId().equals(id)) return c; } return null; } } Со временем ArrayList будет содержать объекты Crime , созданные пользовате- лем, которые будут сохраняться и загружаться повторно. А пока заполним массив 100 однообразными объектами Crime (листинг 9.3). Листинг 9.3. Генерирование тестовых объектов (CrimeLab.java) private CrimeLab(Context appContext) { mAppContext = appContext; mCrimes = new ArrayList(); for (int i = 0; i < 100; i++) { Crime c = new Crime(); c.setTitle("Crime #" + i); c.setSolved(i % 2 == 0); // Для каждого второго объекта mCrimes.add(c); } }