, который умеет создавать, заполнять и воз- вращать представление, определенное в новом макете.
Создание макета элемента списка
В приложении CriminalIntent макет элемента списка должен включать краткое описание преступления, дату и признак раскрытия (рис. 9.11). Такой макет состоит из двух виджетов
TextView и
CheckBox
Новый макет создается точно так же, как для представления активности или фраг- мента. На панели
Package
Explorer щелкните правой кнопкой мыши на каталоге res/
layout и выберите команду
New
Other...
Android
XML
File
. В открывшемся диалоговом окне выберите тип ресурса
Layout
, введите имя файла list_item_crime.xml
, выберите корневой элемент
RelativeLayout и щелкните на кнопке
Finish
Настройка элементов списка
197
RelativeLayout позволяет использовать параме- тры макета для размещения дочерних представ- лений относительно корневого макета и друг друга. Мы хотим, чтобы виджет
CheckBox ав- томатически выравнивался по правой стороне
RelativeLayout
. Два виджета
TextView будут выравниваться относительно
CheckBox
На рис. 9.12 изображены виджеты макета поль- зовательского элемента списка. Виджет
Check-
Box должен определяться первым несмотря на то, что он находится у правого края макета. Это связано с тем, что
TextView будут использовать идентификатор
CheckBox в значении атрибута.
Рис. 9.12. Макет пользовательского элемента списка (list_item_crime.xml)
Рис. 9.11. Список с пользователь- ским форматом элементов
198
Глава 9. Вывод списков и ListFragment
По той же причине определение виджета
TextView с кратким описанием должно предшествовать определению виджета
TextView с датой. В файле макета виджет должен определяться до того, как другие виджеты смогут использовать его иден- тификатор в своих определениях.
Обратите внимание: при использовании идентификатора виджета в определении другого виджета не нужно включать знак
+
. Он используется для создания иденти- фикатора при его первом вхождении в файл макета — обычно в атрибуте android:id
Также обратите внимание на использование строковых литералов вместо строко- вых ресурсов в атрибутах android:text
. Эти текстовые атрибуты содержат фик- тивный текст для разработки и тестирования. Значения, видимые пользователю, предоставляет адаптер. Поскольку эти конкретные строки никогда не будут видны пользователю, создавать для них строковые ресурсы бессмысленно.
Макет пользовательского элемента списка завершен, и мы можем переходить к следующему шагу — созданию пользовательского адаптера.
Создание субкласса адаптера
Наш макет предназначен для отображения элементов списка, представляющих объ- екты
Crime
. Для получения данных этих элементов используются методы доступа
Crime
, так что нам понадобится новый адаптер, умеющий работать с объектами
Crime
В файле
CrimeListFragment.java создайте субкласс
ArrayAdapter как внутренний класс
CrimeListFragment
Листинг 9.18. Добавление пользовательского адаптера как внутреннего класса
(CrimeListFragment.java)
public void onListItemClick(ListView l, View v, int position, long id) {
Crime c = (Crime)(getListAdapter()).getItem(position);
Log.d(TAG, c.getTitle() + " was clicked");
}
private class CrimeAdapter extends ArrayAdapter {
public CrimeAdapter(ArrayList crimes) {
super(getActivity(), 0, crimes);
}
}
}
Вызов конструктора суперкласса необходим для подключения набора данных
Crime
Мы не будем использовать предопределенный макет, поэтому вместо идентифика- тора макета можно передать 0.
Пользовательский элемент списка создается и возвращается в методе
Array-
Adapter
:
public View getView(int position, View convertView, ViewGroup parent)
Параметр convertView содержит существующий элемент списка, который может быть изменен и возвращен адаптером вместо создания нового объекта. Повторное
Настройка элементов списка
199
создание объектов представления повышает быстродействие приложения, потому что оно позволяет избежать постоянного создания и уничтожения однотипных объ- ектов. Количество элементов, одновременно отображаемых в
ListView
, ограничено, поэтому было бы неэффективно содержать множество неиспользуемых объектов представлений, попусту расходующих память.
В классе
CrimeAdapter переопределите метод getView(…)
. Метод должен возвращать представление, полученное заполнением пользовательского макета и содержащее правильные данные
Crime
(листинг 9.19).
Листинг 9.19. Переопределение getView(…) (CrimeListFragment.java)
private class CrimeAdapter extends ArrayAdapter {
public CrimeAdapter(ArrayList crimes) {
super(getActivity(), 0, crimes);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// Если мы не получили представление, заполняем его
if (convertView == null) {
convertView = getActivity().getLayoutInflater()
.inflate(R.layout.list_item_crime, null);
}
// Настройка представления для объекта Crime
Crime c = getItem(position);
TextView titleTextView =
(TextView)convertView.findViewById(R.id.crime_list_item_titleTextView);
titleTextView.setText(c.getTitle());
TextView dateTextView =
(TextView)convertView.findViewById(R.id.crime_list_item_dateTextView);
dateTextView.setText(c.getDate().toString());
CheckBox solvedCheckBox =
(CheckBox)convertView.findViewById(R.id.crime_list_item_solvedCheckBox);
solvedCheckBox.setChecked(c.isSolved());
return convertView;
}
}
В этой реализации getView(…)
мы сначала проверяем, был ли передан существу- ющий объект, предназначенный для переработки. Если нет — объект заполняется по текущему макету.
Как с новым, так и с перерабатываемым объектом вызывается метод getItem(int)
класса
Adapter для получения объекта
Crime в текущей позиции списка.
После получения нужного объекта
Crime мы получаем ссылку на каждый виджет в объекте представления и настраиваем его данными
Crime
. В конце объект пред- ставления возвращается
ListView
Теперь мы можем подключить свой адаптер к
CrimeListFragment
. В начале файла
CrimeListFragment.java обновите реализации onCreate(…)
и onListItemClick(…)
для использования
CrimeAdapter
, как показано в листинге 9.20.
200
Глава 9. Вывод списков и ListFragment
Листинг 9.20. Использование CrimeAdapter (CrimeListFragment.java)
ArrayAdapter adapter = new ArrayAdapter(this,
android.R.layout.simple_list_item_1,
mCrimes);
CrimeAdapter adapter = new CrimeAdapter(mCrimes);
setListAdapter(adapter);
}
public void onListItemClick(ListView l, View v, int position, long id) {
Crime c = (Crime)(getListAdapter()).getItem(position);
Crime c = ((CrimeAdapter)getListAdapter()).getItem(position);
Log.d(TAG, c.getTitle() + " was clicked");
}
Приведение к типу
CrimeAdapter позволяет использовать преимущества проверки типов.
CrimeAdapter может содержать только объекты
Crime
, поэтому преобразова- ние к
Crime становится лишним.
В обычной ситуации приложение было бы готово к запуску. Однако из-за того, что элемент списка содержит
CheckBox
, необходимо внести еще одно изменение. Виджет
CheckBox по умолчанию может получать фокус ввода. Соответственно щелчок на элементе списка будет интерпретироваться как переключение состояния
CheckBox и не достигнет метода onListItemClick(…)
Рис. 9.13. Список с пользовательскими элементами
Настройка элементов списка
201
Эта внутренняя особенность
ListView означает, что для любого виджета с под- держкой фокуса, присутствующего в макете элемента списка (например,
CheckBox или
Button
), получение фокуса следует запретить. Это необходимо для того, чтобы щелчки на элементах списка работали так, как вы ожидаете.
Поскольку элемент
CheckBox используется только для вывода информации и не связывается с логикой приложения, существует простое решение: достаточно внести изменение в файл list_item_crime.xml и запретить передачу фокуса
CheckBox
Листинг 9.21. Запрет передачи фокуса CheckBox (list_item_crime.xml)
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:layout_alignParentRight="true"
android:enabled="false"
android:focusable="false"
android:padding="4dp" />
Запустите приложение и прокрутите список. Щелкните на элементе списка и по журналу убедитесь в том, что
CrimeAdapter возвращает правильные данные. Если приложение запустилось, но макет выглядит неправильно, вернитесь к файлу list_item_crime.xml и проверьте определение макета.
Аргументы фрагментов
В этой главе мы наладим совместную работу списка и детализации в приложении
CriminalIntent. Когда пользователь щелкает на элементе списка преступлений, на экране появляется новый экземпляр
CrimeActivity
, который является хостом для экземпляра
CrimeFragment с подробной информацией о конкретном экземпляре
Crime
Рис. 10.1. Запуск CrimeActivity из CrimeListActivity
В приложении GeoQuiz одна активность (
QuizActivity
) запускала другую (
CheatAc- tivity
). В приложении CriminalIntent активность
CrimeActivity будет запускаться из фрагмента
CrimeListFragment
Запуск активности из фрагмента
Запуск активности из фрагмента осуществляется практически так же, как запуск ак- тивности из другой активности. Вы вызываете метод
Fragment.startActivity(Intent)
, который вызывает соответствующий метод
Activity во внутренней реализации.
В реализации onListItemClick(…)
из
CrimeListFragment замените регистрацию краткого описания кодом, запускающим экземпляр
CrimeActivity
. (Не обращайте внимания на предупреждение о неиспользуемой переменной
Crime
; мы используем ее в следующем разделе.)
10
Запуск активности из фрагмента
203
Листинг 10.1. Запуск CrimeActivity (CrimeListFragment.java)
public void onListItemClick(ListView l, View v, int position, long id) {
// Получение объекта Crime от адаптера
Crime c = ((CrimeAdapter)getListAdapter()).getItem(position);
Log.d(TAG, c.getTitle() + " was clicked");
// Запуск CrimeActivity
Intent i = new Intent(getActivity(), CrimeActivity.class);
startActivity(i);
}
Здесь класс
CrimeListFragment создает явный интент с указанием класса
CrimeAc- tivity
CrimeListFragment использует метод getActivity()
для передачи актив- ности-хоста как объекта
Context
, необходимого конструктору
Intent
Запустите приложение CriminalIntent. Щелкните на любой строке списка; откры- вается новый экземпляр
CrimeActivity
, управляющий фрагментом
CrimeFragment
Рис. 10.2. Пустой экземпляр CrimeFragment
Экземпляр
CrimeFragment еще не содержит данных конкретного объекта
Crime
, потому что мы не сообщили ему, какой именно объект
Crime следует отображать.
Включение дополнения
Чтобы сообщить
CrimeFragment
, какой объект
Crime следует отображать, можно сделать mCrimeId дополнением (extra) объекта
Intent
. В методе onListItemClick(…)
204Глава 10. Аргументы фрагментов включите поле mCrimeId выбранного объекта
Crime в интент, запускающий
Crime-
Activity
В следующем коде Eclipse обнаруживает ошибку — ключ
CrimeFragment.EXTRA_
CRIME_ID
еще не создан. Не обращайте внимания на ошибку, вскоре мы создадим его.
Листинг 10.2. Запуск CrimeActivity с дополнением (CrimeListFragment.java)
public void onListItemClick(ListView l, View v, int position, long id) {
// Получение объекта Crime от адаптера
Crime c = ((CrimeAdapter)getListAdapter()).getItem(position);
// Запуск CrimeActivity
Intent i = new Intent(getActivity(), CrimeActivity.class);
i.putExtra(CrimeFragment.EXTRA_CRIME_ID, c.getId()); startActivity(i);
}
После создания явного интента мы вызываем putExtra(…)
, передавая строковый ключ и связанное с ним значение (
mCrimeId
). В данном случае вызывается версия putExtra(String,
Serializable)
, потому что UUID является объектом
Serializable
Чтение дополненияПоле mCrimeId сохранено в интенте, принадлежащем
CrimeActivity
, однако про- читать и использовать эти данные должен класс
CrimeFragment
Существует два способа, которыми фрагмент может обратиться к данным из ин- тента активности: простое и прямолинейное обходное решение и сложная, гибкая полноценная реализация. Сначала мы опробуем первый способ, а потом реализуем сложное гибкое решение с
аргументами фрагментов.
В простом решении
CrimeFragment просто использует метод getActivity()
для прямо- го обращения к интенту
CrimeActivity
. Вернитесь к классу
CrimeFragment и
добавьте ключ для дополнения, затем в методе onCreate(Bundle)
прочитайте дополнительный из интента
CrimeActivity и используйте его для получения данных
Crime
Листинг 10.3. Получение дополнения и выборка Crime (CrimeFragment.java)
public class CrimeFragment extends Fragment {
public static final String EXTRA_CRIME_ID = "com.bignerdranch.android.criminalintent.crime_id"; private Crime mCrime;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
UUID crimeId = (UUID)getActivity().getIntent() .getSerializableExtra(EXTRA_CRIME_ID); mCrime = CrimeLab.get(getActivity()).getCrime(crimeId);}
Запуск активности из фрагмента
205
Если не считать вызова getActivity()
, листинг 10.3 практически не отличается от кода выборки дополнения из кода активности. Метод getIntent()
возвра- щает объект
Intent
, используемый для запуска
CrimeActivity
. Мы вызываем getSerializableExtra(String)
для
Intent
, чтобы извлечь UUID в переменную.
После получения идентификатора мы используем его для получения объекта
Crime от
CrimeLab
. Методу
CrimeLab.get(…)
должен передаваться объект
Context
, поэтому
CrimeFragment передает
CrimeActivity
Обновление представления CrimeFragment данными Crime
Теперь, когда фрагмент
CrimeFragment получает объект
Crime
, его представление может отобразить данные
Crime
. Обновите метод onCreateView(…)
, чтобы он выво- дил краткое описание преступления и признак раскрытия (код вывода даты уже имеется).
Рис. 10.3. Преступление, выбранное в списке
Листинг 10.4. Обновление объектов представления (CrimeFragment.java)
@Override public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
mTitleField = (EditText)v.findViewById(R.id.crime_title);
продолжение
206
Глава 10. Аргументы фрагментов
Листинг 10.4 (продолжение)
mTitleField.setText(mCrime.getTitle());
mTitleField.addTextChangedListener(new TextWatcher() {
});
mSolvedCheckBox = (CheckBox)v.findViewById(R.id.crime_solved);
mSolvedCheckBox.setChecked(mCrime.isSolved());
mSolvedCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
});
return v;
}
Запустите приложение CriminalIntent. Выберите строку
Crime
#4
и убедитесь в том, что на экране появляется экземпляр
CrimeFragment с правильными данными пре- ступления.
Недостаток прямой выборки
Обращение из фрагмента к интенту, принадлежащему активности-хосту, упрощает код. С другой стороны, оно нарушает инкапсуляцию фрагмента. Класс
CrimeFragment уже не является структурным элементом, пригодным для повторного использова- ния, потому что он предполагает, что его хостом всегда будет активность с объектом
Intent
, определяющим дополнение с именем
EXTRA_CRIME_ID
Возможно, для
CrimeFragment такое предположение разумно, но оно означает, что класс
CrimeFragment в своей текущей реализации не может использоваться с про- извольной активностью.
Другое, более правильное решение — сохранение значения mCrimeId в месте, при- надлежащем
CrimeFragment
(вместо хранения его в личном пространстве
CrimeAc- tivity
). В этом случае объект
CrimeFragment может прочитать данные, не полагаясь на присутствие конкретного дополнения в интенте активности. Такое «место», принадлежащее фрагменту, называется пакетом аргументов (arguments bundle).
Аргументы фрагментов
К каждому экземпляру фрагмента может быть прикреплен объект
Bundle
. Этот объект содержит пары «ключ-значение», которые работают так же, как дополнения интентов
Activity
. Каждая такая пара называется аргументом (argument).
Чтобы создать аргументы фрагментов, вы сначала создаете объект
Bundle
, а затем используете put
-методы
Bundle соответствующего типа (по аналогии с методами
Intent
) для добавления аргументов в пакет.
Bundle args = new Bundle();
args.putSerializable(EXTRA_MY_OBJECT, myObject);
args.putInt(EXTRA_MY_INT, myInt);
args.putCharSequence(EXTRA_MY_STRING, myString);
Запуск активности из фрагмента
207Присоединение аргументов к фрагментуЧтобы присоединить пакет аргументов к фрагменту, вызовите метод
Fragment.
setArguments(Bundle)
. Присоединение должно быть выполнено после создания фрагмента, но до его добавления в активность.
Для этого программисты Android используют схему с добавлением в класс
Fragment статического метода с именем newInstance()
. Этот метод создает экземпляр фраг- мента, упаковывает и задает его аргументы.
Когда активности-хосту потребуется экземпляр этого фрагмента, она вместо прямого вызова конструктора вызывает метод newInstance()
. Активность может передать newInstance(…)
любые параметры, необходимые фрагменту для создания аргументов.
Включите в
CrimeFragment метод newInstance(UUID)
, который получает UUID, создает пакет аргументов,
создает экземпляр фрагмента, а затем присоединяет аргументы к фрагменту.
Листинг 10.5. Метод newInstance(UUID) (CrimeFragment.java)
public static CrimeFragment newInstance(UUID crimeId) { Bundle args = new Bundle(); args.putSerializable(EXTRA_CRIME_ID, crimeId); CrimeFragment fragment = new CrimeFragment(); fragment.setArguments(args); return fragment;}Теперь класс
CrimeActivity должен вызывать
CrimeFragment.newInstance(UUID)
каждый раз, когда ему потребуется создать
CrimeFragment
. При вызове передается значение UUID, полученное из дополнения. Вернитесь к классу
CrimeActivity
, в методе createFragment()
получите дополнение из интента
CrimeActivity и пере- дайте его
CrimeFragment.newInstance(UUID)
Листинг 10.6. Использование newInstance(UUID) (CrimeActivity.java)
@Override protected Fragment createFragment() {
return new CrimeFragment(); UUID crimeId = (UUID)getIntent() .getSerializableExtra(CrimeFragment.EXTRA_CRIME_ID); return CrimeFragment.newInstance(crimeId);}
Учтите, что потребность в независимости не является двусторонней. Класс
CrimeAc- tivity должен многое знать о классе
CrimeFragment
— например, то, что он содержит метод newInstance(UUID)
. Это нормально; активность-хост должна располагать конкретной информацией о том, как управлять фрагментами, но фрагментам такая информация об их активности не нужна (по крайней мере если вы хотите сохранить гибкость независимых фрагментов).