android:id="@+id/fragmentContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
Хотя activity_crime.xml состоит исключительно из контейнерного представления одно- го фрагмента, макет активности может быть более сложным; он может определять несколько контейнерных представлений, а также собственные виджеты.
Просмотрите файл макета или запустите CriminalIntent, чтобы проверить свой код.
Вы увидите только пустой элемент
FrameLayout
, потому что
CrimeActivity еще не выполняет функции хоста фрагмента.
Рис. 7.13. Пустой элемент FrameLayout
Позднее мы напишем код, помещающий представление фрагмента в этот элемент
FrameLayout
. Но сначала фрагмент нужно создать.
Хостинг UI-фрагментов
151Создание UI-фрагментаПоследовательность действий по созданию UI-фрагмента не отличается от после- довательности действий по созданию активности:
построение интерфейса посредством определения виджетов в файле макета;
создание класса и назначение макета, который был определен ранее, его пред- ставлением;
подключение виджетов, заполненных на основании макета в коде.
Определение макета CrimeFragmentПредставление
CrimeFragment будет отображать информацию, содержащуюся в эк- земпляре
Crime
. Со временем в классе
Crime и представлении класса
CrimeFragment добавится много интересного, а в этой главе мы ограничимся текстовым полем для хранения заголовка.
На рис. 7.14 изображен макет представления
CrimeFragment
. Он состоит из верти- кального элемента
LinearLayout
, содержащего
EditText
— виджет с областью для ввода и редактирования текста.
Рис. 7.14. Исходный макет CrimeFragment
Чтобы
создать файл макета, щелкните правой кнопкой мыши на папке res/layout на панели
Package
Explorer и выберите команду
New
Android
XML
File
. Проследите за тем, чтобы в окне был выбран тип ресурса
Layout
, а файлу фрагмента было присвоено имя fragment_crime.xml
. Выберите корневым элементом
LinearLayout и нажмите кнопку
Finish
Когда файл откроется, перейдите к разметке XML. Мастер уже добавил элемент
LinearLayout за вас. Руководствуясь рис. 7.14, внесите необходимые изменения в fragment_crime.xml
. Проверьте результаты по листингу 7.5.
152
Глава 7. UI-фрагменты и FragmentManager
Листинг 7.5. Файл макета для представления фрагмента (fragment_crime.xml)
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/crime_title_hint"
/>
Откройте файл res/values/strings.xml
, добавьте строковый ресурс crime_title_hint и уалите лишние строковые ресурсы hello_world и menu_settings
, сгенерирован- ные шаблоном.
Листинг 7.6. Добавление и удаление строк (res/values/strings.xml)
CriminalIntent
Hello world!
Settings
CrimeActivity
Enter a title for the crime.
Сохраните файлы. Удаление строки menu_settings привело к возникновению ошибки в проекте. Чтобы исправить ее, найдите на панели
Package
Explorer файл res/menu/activity_crime.xml
. Этот файл содержит определение меню, сгенерированное шаблоном, содержащее ссылку на строку menu_settings
. Мы не будем использовать этот файл меню для CriminalIntent, поэтому его можно просто удалить с панели
Package
Explorer
Удаление ресурса меню заставляет Eclipse построить приложение заново. Теперь проект должен быть свободен от ошибок. Переключитесь на графический конструк- тор, чтобы просмотреть результат разметки fragment_crime.xml
Создание класса CrimeFragment
Щелкните правой кнопкой мыши на пакете com.bignerdranch.android.criminalintent и выберите команду
New
Class
. Введите имя класса
CrimeFragment и щелкните на кнопке
Browse
, чтобы выбрать суперкласс. В открывшемся окне начинайте вводить строку
Fragment
. Мастер предлагает несколько классов. Выберите класс android.
support.v4.app.Fragment
— класс
Fragment из библиотеки поддержки. Щелкните на кнопке
OK
(Если вы видите несколько версий android.support.v4.app.Fragment
, выберите ту, которая является частью проекта CriminalIntent из
CriminalIntent/libs/
.)
Хостинг UI-фрагментов
153Рис. 7.15. Выбор класса Fragment из библиотеки поддержки
Реализация методов жизненного цикла фрагментаCrimeFragment
— контроллер, взаимодействующий с объектами модели и представ- ления. Его задача — выдача подробной информации о конкретном преступлении и ее обновление при модификации пользователем.
В приложении GeoQuiz активности выполняли большую часть работы контрол- лера в методах жизненного цикла. В приложении CriminalIntent эта работа будет выполняться фрагментами в методах жизненного цикла фрагментов. Многие из этих методов соответствуют уже известным вам методам
Activity
, таким как onCreate(Bundle)
В файле
CrimeFragment.java добавьте переменную для экземпляра
Crime и реализацию
Fragment.onCreate(Bundle)
Листинг 7.7. Переопределение Fragment.onCreate(Bundle) (CrimeFragment.java)
public class CrimeFragment extends Fragment {
private Crime mCrime; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mCrime = new Crime(); }}
154
Глава 7. UI-фрагменты и FragmentManager
В этой реализации стоит обратить внимание на пару моментов. Во-первых, ме- тод
Fragment.onCreate(Bundle)
объявлен открытым, тогда как метод
Activity.
onCreate(Bundle)
объявлен защищенным.
Fragment.onCreate(…)
и другие методы жизненного цикла
Fragment должны быть открытыми, потому что они будут вы- зываться произвольной активностью, которая станет хостом фрагмента.
Во-вторых, как и в случае с активностью, фрагмент использует объект
Bundle для сохранения и загрузки состояния. Вы можете переопределить
Fragment.on-
Save InstanceState(Bundle)
для ваших целей, как и метод
Activity.onSave-
InstanceState(Bundle)
Обратите внимание на то, что не происходит в
Fragment.onCreate(…)
: мы не запол- няем представление фрагмента. Экземпляр фрагмента настраивается в
Fragment.
onCreate(…)
, но создание и настройка представления фрагмента осуществляются в другом методе жизненного цикла фрагмента:
public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState)
Именно в этом методе заполняется макет представления фрагмента, а заполненный объект
View возвращает активность хосту. Параметры
LayoutInflater и
ViewGroup необходимы для заполнения макета. Объект
Bundle содержит данные, которые ис- пользуются методом для воссоздания представления по сохраненному состоянию.
В файле
CrimeFragment.java добавьте реализацию onCreateView(…)
, которая заполняет разметку fragment_crime.xml
Листинг 7.8. Переопределение onCreateView(…) (CrimeFragment.java)
public class CrimeFragment extends Fragment {
private Crime mCrime;
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mCrime = new Crime();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_crime, parent, false);
return v;
}
}
В методе onCreateView(…)
мы явно заполняем представление фрагмента, вызывая
LayoutInflater.inflate(…)
с передачей идентификатора ресурса макета. Второй па- раметр определяет родителя представления, что обычно необходимо для правильной настройки виджета. Третий параметр указывает, нужно ли включать заполненное представление в родителя. Мы передаем false
, потому что представление будет добавлено в коде активности.
Хостинг UI-фрагментов
155Подключение виджетов в фрагментеВ методе onCreateView(…)
также настраивается реакция виджета
EditText на ввод пользователя. После того как представление будет заполнено, метод получает ссылку на
EditText и добавляет слушателя.
Листинг 7.9. Настройка виджета EditText (CrimeFragment.java)
public class CrimeFragment extends Fragment {
private Crime mCrime;
private EditText mTitleField; @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_crime, parent, false);
mTitleField = (EditText)v.findViewById(R.id.crime_title); mTitleField.addTextChangedListener(new TextWatcher() { public void onTextChanged( CharSequence c, int start, int before, int count) { mCrime.setTitle(c.toString()); } public void beforeTextChanged( CharSequence c, int start, int count, int after) { // Здесь намеренно оставлено пустое место } public void afterTextChanged(Editable c) { // И здесь тоже } }); return v;
}
}
Получение ссылок в
Fragment.onCreateView(…)
происходит практически так же, как в
Activity.onCreate(…)
. Единственное различие заключается в том, что для пред- ставления фрагмента вызывается метод
View.findViewById(int)
. Метод
Activity.
findViewById(int)
, который мы использовали ранее, является вспомогательным методом, который вызывает
View.findViewById(int)
в своей внутренней реализации.
У класса
Fragment аналогичного вспомогательного метода нет, поэтому приходится вызывать основной метод.
Назначение слушателей в фрагменте работает точно так же, как в активности. В ли- стинге 7.9 мы создаем анонимный класс, который реализует интерфейс слушателя
TextWatcher
. Этот интерфейс содержит три метода, но нас интересует только один: onTextChanged(…)
В методе onTextChanged(…)
мы вызываем toString()
для объекта
CharSequence
, представляющего ввод пользователя. Этот метод возвращает строку, которая затем используется для задания заголовка
Crime
156Глава 7. UI-фрагменты и FragmentManager
Код
CrimeFragment готов.
Было бы замечательно, если бы вы могли запустить
CriminalIntent и поэкспериментировать с написанным кодом. К сожалению, это невозможно — фрагменты не могут выводить свои представления на экран.
Чтобы реализовать задуманное, необходимо сначала добавить
CrimeFragment в
CrimeActivity
Добавление UI-фрагмента в FragmentManagerКогда в Honeycomb появился класс
Fragment
, в класс
Activity были внесены изменения: в него был добавлен компонент, называемый
FragmentManager
. Он от- вечает за управление фрагментами и добавление их представлений в иерархию представлений активности.
FragmentManager управляет двумя структурами: списком фрагментов и стеком транзакций фрагментов (о котором я вскоре расскажу).
Фрагменты
Стек транзакций
Рис. 7.16. FragmentManager
В приложении CriminalIntent нас интересует только список фрагментов
Fragment-
Manager
Чтобы добавить фрагмент в активность в коде, следует обратиться с вызовом к объ- екту
FragmentManager активности.
Прежде всего необходимо получить сам объект
FragmentManager
. В
CrimeActivity.java включите следующий код в onCreate(…)
Листинг 7.10. Получение объекта FragmentManager (CrimeActivity.java)
public class CrimeActivity extends FragmentActivity {
/** Вызывается при создании активности. */
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crime);
FragmentManager fm = getSupportFragmentManager(); }
}
Хостинг UI-фрагментов
157
Мы вызываем getSupportFragmentManager()
, потому что в приложении используется библиотека поддержки и класс
FragmentActivity
. Если бы нас не интересовала со- вместимость с устройствами, предшествующими Honeycomb, то вместо этого можно было бы субклассировать
Activity и вызвать getFragmentManager()
Транзакции фрагментов
После получения объекта
FragmentManager добавьте следующий код, который передает ему фрагмент для управления. (Позднее мы рассмотрим этот код более подробно, а пока просто включите его в приложение.)
Листинг 7.11. Добавление CrimeFragment (CrimeActivity.java)
public class CrimeActivity extends FragmentActivity {
/** Вызывается при создании активности. */
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crime);
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragmentContainer);
if (fragment == null) {
fragment = new CrimeFragment();
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment)
.commit();
}
}
}
Разбираться в коде, добавленном в листинге 7.11, лучше всего не с начала. Найдите операцию add(…)
и окружающий ее код. Этот код создает и закрепляет транзакцию
фрагмента
Листинг 7.12. Транзакция фрагмента (CrimeActivity.java)
if (fragment == null) {
fragment = new CrimeFragment();
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment)
.commit();
Транзакции фрагментов используются для добавления, удаления, присоединения, отсоединения и замены фрагментов в списке фрагментов. Они лежат в основе ме- ханизма использования фрагментов для формирования и модификации экранов во время выполнения.
FragmentManager ведет стек транзакций, по которому вы можете перемещаться.
Метод
FragmentManager.beginTransaction()
создает и возвращает экзем- пляр
FragmentTransaction
. Класс
FragmentTransaction использует динамич- ный интерфейс — методы, настраивающие
FragmentTransaction
, возвращают
158
Глава 7. UI-фрагменты и FragmentManager
FragmentTransaction вместо void
, что позволяет объединять их вызовы в цепочку.
Таким образом, выделенный код в листинге 7.12 означает: «Создать новую тран- закцию фрагмента, включить в нее одну операцию add
, а затем закрепить».
Метод add(…)
является основным содержанием транзакции. Он получает два параметра: идентификатор контейнерного представления и недавно созданный объект
CrimeFragment
. Идентификатор контейнерного представления вам должен быть знаком: это идентификатор ресурса элемента
FrameLayout
, определенного в файле activity_crime.xml
. Идентификатор контейнерного представления выполняет две функции:
Он сообщает
FragmentManager
, где в представлении активности должно нахо- диться представление фрагмента.
Он обеспечивает однозначную идентификацию фрагмента в списке
Fragment-
Manager
Когда вам потребуется получить экземпляр
CrimeFragment от
FragmentManager
, за- просите его по идентификатору контейнерного представления.
Листинг 7.13. Получение существующего фрагмента по идентификатору контейнерного представления (CrimeActivity.java)
FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragmentContainer);
if (fragment == null) {
fragment = new CrimeFragment();
fm.beginTransaction()
.add(R.id.fragmentContainer, fragment)
.commit();
}
Может показаться странным, что
FragmentManager идентифицирует
CrimeFragment по идентификатору ресурса
FrameLayout
. Однако идентификация UI-фрагмента по идентификатору ресурса его контейнерного представления встроена в механизм работы
FragmentManager
Теперь мы можем кратко описать код, добавленный в листинг 7.11, от начала до конца.
Сначала мы запрашиваем у
FragmentManager фрагмент с идентификатором контей- нерного представления
R.id.fragmentContainer
. Если этот фрагмент уже находится в списке,
FragmentManager возвращает его.
Почему фрагмент может уже находиться в списке? Вызов
CrimeActivity.onCre- ate(…)
может быть выполнен в ответ на воссоздание объекта
CrimeActivity после его уничтожения из-за поворота или освобождения памяти. При уничтожении активности ее экземпляр
FragmentManager сохраняет список фрагментов. При вос- создании активности новый экземпляр
FragmentManager загружает список и вос- создает хранящиеся в нем фрагменты, чтобы все работало, как прежде.
С другой стороны, если фрагменты с заданным идентификатором контейнерно- го представления отсутствуют, значение fragment равно null
. В этом случае мы
Хостинг UI-фрагментов
159
создаем новый экземпляр
CrimeFragment и новую транзакцию, которая добавляет фрагмент в список.
Теперь
CrimeActivity является хостом для
CrimeFragment
. Чтобы убедиться в этом, запустите приложение CriminalIntent. На экране отображается представление, определенное в файле fragment_crime.xml
(рис. 7.17).
Рис. 7.17. CrimeActivity является хостом представления CrimeFragment
Возможно, один виджет на экране выглядит не таким уж большим достижением для всей работы, проделанной в этой главе. Однако мы заложили прочный фун- дамент для более серьезных задач, которые нам придется решать в приложении
CriminalIntent в последующих главах.
FragmentManager и жизненный цикл фрагмента
После знакомства с
FragmentManager стоит еще раз вернуться к жизненному циклу фрагмента.
Объект
FragmentManager активности отвечает за вызов методов жизненного цикла фрагментов в списке. Методы onAttach(Activity)
, onCreate(Bundle)
и onCreateV- iew(…)
вызываются при добавлении фрагмента в
FragmentManager
Метод onActivityCreated(…)
вызывается после выполенния метода onCreate(…)
активности-хоста. Мы добавляем
CrimeFragment в
CrimeActivity.onCreate(…)
, так что этот метод будет вызван после добавления фрагмента.