Глава 22. Двухпанельные интерфейсы Чтобы определить, интерфейс какого типа был заполнен, можно проверить кон- кретный идентификатор интерфейса. Впрочем, лучше проверить наличие в ма- кете detailFragmentContainer . Такая проверка будет более точной и надежной. Имена файлов могут изменяться, и вас на самом деле не интересует, по какому файлу заполнялся макет; необходимо знать лишь то, имеется ли у него контейнер detailFragmentContainer для размещения CrimeFragment Если макет содержит detailFragmentContainer , мы создадим транзакцию фраг- мента, которая удаляет существующий экземпляр CrimeFragment из detail- FragmentContainer (если он имеется) и добавляет экземпляр CrimeFragment , который мы хотим там видеть. В файле CrimeListActivity.java реализуйте метод onCrimeSelected(Crime) , который будет обрабатывать выбор преступления в любом варианте интерфейса. Листинг 22.9. Условный запуск CrimeFragment (CrimeListActivity.java) public void onCrimeSelected(Crime crime) { if (findViewById(R.id.detailFragmentContainer) == null) { // Запуск экземпляра CrimePagerActivity Intent i = new Intent(this, CrimePagerActivity.class); i.putExtra(CrimeFragment.EXTRA_CRIME_ID, crime.getId()); startActivity(i); } else { FragmentManager fm = getSupportFragmentManager(); FragmentTransaction ft = fm.beginTransaction(); Fragment oldDetail = fm.findFragmentById(R.id.detailFragmentContainer); Fragment newDetail = CrimeFragment.newInstance(crime.getId()); if (oldDetail != null) { ft.remove(oldDetail); } ft.add(R.id.detailFragmentContainer, newDetail); ft.commit(); } } Наконец, в классе CrimeListFragment мы будем вызывать onCrimeSelected(Crime) в тех местах, где сейчас запускается новый экземпляр CrimePagerActivity В файле CrimeListFragment.java измените методы onListItemClick(…) и onOptions- ItemSelected(MenuItem) так, чтобы в них вызывался метод Callbacks. onCrimeSelected(Crime) Листинг 22.10. Активизация обратных вызовов (CrimeListFragment.java) public void onListItemClick(ListView l, View v, int position, long id) { // Получение объекта Crime от адаптера Crime c = ((CrimeAdapter)getListAdapter()).getItem(position); // Запуск экземпляра CrimePagerActivity Intent i = new Intent(getActivity(), CrimePagerActivity.class); i.putExtra(CrimeFragment.EXTRA_CRIME_ID, c.getId()); startActivity(i); mCallbacks.onCrimeSelected(c); } Активность: управление фрагментами 377@TargetApi(11) @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.menu_item_new_crime: Crime crime = new Crime(); CrimeLab.get(getActivity()).addCrime(crime); Intent i = new Intent(getActivity(), CrimePagerActivity.class); i.putExtra(CrimeFragment.EXTRA_CRIME_ID, crime.getId()); startActivity(i); ((CrimeAdapter)getListAdapter()).notifyDataSetChanged(); mCallbacks.onCrimeSelected(crime); return true; } } При обратном вызове в onOptionsItemSelected(…) содержимое списка также немед- ленно перезагружается после добавления нового преступления. Это необходимо, потому что на планшетах при добавлении нового преступления список остается видимым на экране (прежде его закрывал экран детализации). Запустите приложение CriminalIntent на планшете. Создайте новое преступление; экземпляр CrimeFragment добавляется и отображается в detailFragmentContainer Затем просмотрите какое-либо старое преступление и убедитесь в том, что Crime- Fragment заменяется новым экземпляром. Рис. 22.6. Главное и детализированное представления связаны между собойОднако внесение изменений в преступление не приводит к обновлению списка. В настоящее время список перезагружается только после добавления престу- пления и в CrimeListFragment.onResume() . При этом на планшетах экземпляр 378 Глава 22. Двухпанельные интерфейсы CrimeListFragment остается видимым рядом с CrimeFragment CrimeListFragment не приостанавливается при появлении CrimeFragment , поэтому и возобновления не происходит. Для решения этой проблемы мы используем другой интерфейс обратного вызова из CrimeFragment Реализация CrimeFragment.Callbacks CrimeFragment определяет следующий интерфейс: public interface Callbacks { void onCrimeUpdated(Crime crime); } CrimeFragment будет вызывать метод onCrimeUpdated(Crime) активности-хоста при сохранении любых изменений в Crime . Реализация onCrimeUpdated(Crime) в CrimeListActivity перезагружает список CrimeListFragment Прежде чем браться за интерфейс в CrimeFragment , добавьте в CrimeListFragment метод, который вызывается для перезагрузки списка CrimeListFragment Листинг 22.11. Добавление метода updateUI() (CrimeListFragment.java) public class CrimeListFragment extends ListFragment { public void updateUI() { ((CrimeAdapter)getListAdapter()).notifyDataSetChanged(); } В файле CrimeFragment.java добавьте интерфейс обратного вызова, а также поле mCallbacks и реализации onAttach(…) и onDetach() Листинг 22.12. Добавление обратных вызовов CrimeFragment (CrimeFragment.java) private ImageView mPhotoView; private Button mSuspectButton; private Callbacks mCallbacks; /** * Обязательный интерфейс для активности-хоста */ public interface Callbacks { void onCrimeUpdated(Crime crime); } @Override public void onAttach(Activity activity) { super.onAttach(activity); mCallbacks = (Callbacks)activity; } @Override public void onDetach() { super.onDetach();
Активность: управление фрагментами 379 mCallbacks = null; } public static CrimeFragment newInstance(UUID crimeId) { } Затем реализуйте CrimeFragment.Callbacks в CrimeListActivity , чтобы список перезагружался в onCrimeUpdated(Crime) Листинг 22.13. Обновление списка преступлений (CrimeListActivity.java) public void onCrimeUpdated(Crime crime) { FragmentManager fm = getSupportFragmentManager(); CrimeListFragment listFragment = (CrimeListFragment) fm.findFragmentById(R.id.fragmentContainer); listFragment.updateUI(); } В файле CrimeFragment.java добавьте вызовы onCrimeUpdated(Crime) при изменении заголовка или флага раскрытия Crime Листинг 22.14. Вызов onCrimeUpdated(Crime) (CrimeFragment.java) @Override @TargetApi(11) 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.setText(mCrime.getTitle()); mTitleField.addTextChangedListener(new TextWatcher() { public void onTextChanged(CharSequence c, int start, int before, int count) { mCrime.setTitle(c.toString()); mCallbacks.onCrimeUpdated(mCrime); getActivity().setTitle(mCrime.getTitle()); } }); mSolvedCheckBox = (CheckBox)v.findViewById(R.id.crime_solved); mSolvedCheckBox.setChecked(mCrime.isSolved()); mSolvedCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { // Задание признака раскрытия преступления mCrime.setSolved(isChecked); mCallbacks.onCrimeUpdated(mCrime); } }); return v; }
380 Глава 22. Двухпанельные интерфейсы Также необходимо вызвать onCrimeUpdated(Crime) в методе onActivityResult(…) , где могут измениться дата, фотография и подозреваемый в преступлении. В на- стоящее время фотография и подозреваемый не отображаются в представлении элемента списка, но класс CrimeFragment все равно должен вести себя корректно и сообщать об этих обновлениях. Листинг 22.15. Вызов onCrimeUpdated(Crime) (№2) (CrimeFragment.java) @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode != Activity.RESULT_OK) return; if (requestCode == REQUEST_DATE) { Date date = (Date)data.getSerializableExtra(DatePickerFragment.EXTRA_DATE); mCrime.setDate(date); mCallbacks.onCrimeUpdated(mCrime); updateDate(); } else if (requestCode == REQUEST_PHOTO) { // Создание нового объекта Photo и связывание его с Crime String filename = data .getStringExtra(CrimeCameraFragment.EXTRA_PHOTO_FILENAME); if (filename != null) { Photo p = new Photo(filename); mCrime.setPhoto(p); mCallbacks.onCrimeUpdated(mCrime); showPhoto(); } } else if (requestCode == REQUEST_CONTACT) { c.moveToFirst(); String suspect = c.getString(0); mCrime.setSuspect(suspect); mCallbacks.onCrimeUpdated(mCrime); mSuspectButton.setText(suspect); c.close(); } } CrimeListActivity теперь содержит реализацию CrimeFragment.Callbacks . Однако при попытке запустить CriminalIntent на телефоне произойдет сбой. Помните: любая активность, являющаяся хостом CrimeFragment , должна реализовать CrimeFragment. Callbacks . Необходимо реализовать CrimeFragment.Callbacks в CrimePagerActivity Для CrimePagerActivity достаточно пустой реализации, в которой onCrime- Updated(Crime) не делает ничего. Когда CrimePagerActivity является хостом CrimeFragment , необходимая перезагрузка списка уже выполняется в onResume() Листинг 22.16. Пустая реализация CrimeFragment.Callbacks (CrimePagerActivity.java) public class CrimePagerActivity extends FragmentActivity implements CrimeFragment.Callbacks { ... public void onCrimeUpdated(Crime crime) { } }
Для любознательных: подробнее об определении размера экрана 381Запустите приложение CriminalIntent на планшете и убедитесь в том, что List- View обновляется при внесении изменений в CrimeFragment . Затем запустите его на телефоне и убедитесь в том, что приложение продолжает работать, как прежде. Рис. 22.7. Изменения, внесенные в детализированном представлении, отражаются в списке На этом наше знакомство с CriminalIntent подходит к концу. На протяжении 13 глав мы создали сложное приложение, которое использует фрагменты, взаимодействует с другими приложениями, делает снимки и сохраняет данные. Почему бы не от- праздновать окончание работы куском торта? Только не забудьте убрать за собой крошки, чтобы ваш проступок не попал в CriminalIntent. Для любознательных: подробнее об определении размера экранаДо выхода Android 3.2 для предоставления альтернативных ресурсов в зависимости от размера устройства использовался квалификатор размера экрана. Этот квалификатор группирует разные устройства на четыре категории — small , normal , large и xlarge В табл. 22.1 приведены минимальные размеры для каждого квалификатора. Таблица 22.1. Квалификаторы размера экрана ИмяМинимальный размер экранаsmall 320 x 426dp normal 320 x 470dp large 480 x 640dp xlarge 720 x 960dp 382Глава 22. Двухпанельные интерфейсы Квалификаторы размера экрана были объявлены устаревшими в Android 3.2. Они были заменены квалификаторами, позволяющими определять размеры устройства. В табл. 22.2 перечислены эти новые квалификаторы. Таблица 22.2. Дискретные квалификаторы размера экрана Формат квалификатораОписаниеwXXXdp Доступная ширина: ширина больше либо равна XXX dp hXXXdp Доступная высота: высота больше либо равна XXX dp swXXXdp Минимальная ширина: ширина или высота (меньшая из двух) больше либо равна XXX dp Предположим, вы хотите задать макет, который должен использоваться только в том случае, если ширина экрана не менее 300dp. В этом случае можно поместить файл макета в каталог res/layout-w300dp («w» — сокращение от «width», то есть «ширина»). То же самое можно сделать для высоты при помощи префикса «h» («Height», то есть «высота»). Впрочем, ширина и высота могут меняться местами в зависимости от ориентации устройства. Для обнаружения конкретного размера экрана используется префикс «sw» («Smallest Width», то есть «минимальная ширина»). Он задает наименьший размер экрана, которым в зависимости от ориентации устройства может быть как ширина, так и высота. Если размеры экрана равны 1024 × 800, то метрика sw равна 800. Если размеры экрана равны 800 × 1024, то метрика sw все равно равна 800. Подробнее об интентах и задачах В этой главе мы используем неявные интенты для создания приложения-лаунчера, заменяющего стандартный лаунчер Android. Чтобы приложение работало правиль- но, нам придется углубить свое понимание интентов, фильтров интентов и схем взаимодействий между приложениями в среде Android. Создание приложения NerdLauncher Создайте новый проект ( New Android Application Project ) с теми же параметрами, которые использовались для CriminalIntent (рис. 23.1). Присвойте проекту имя NerdLauncher и создайте его в пакете com.bignerdranch.android.nerdlauncher Создайте активность, но без пользовательского значка лаунчера. Выберите созда- ние новой пустой активности. Присвойте активности имя NerdLauncherActivity и щелкните на кнопке Finish Класс NerdLauncherActivity должен быть субклассом SingleFragmentActivity , по- этому этот класс необходимо добавить в проект. На панели Package Explorer найдите файл SingleFragmentActivity.java в пакете CriminalIntent . Скопируйте его в пакет com. bignerdranch.android.nerdlauncher . При копировании файлов Eclipse автомати- чески обновляет объявления пакетов. Нам также понадобится макет activity_fragment.xml . Скопируйте res/layout/activity_frag- ment.xml в каталог res/layout проекта NerdLauncher. NerdLauncher будет отображать список приложений на устройстве. Пользователь нажимает элемент списка, чтобы запустить соответствующее приложение. А теперь посмотрим, какие объекты для этого понадобятся. 23
384Глава 23. Подробнее об интентах и задачахРис. 23.1. Создание проекта NerdLauncher Класс NerdLauncherFragment является субклассом ListFragment , а в качестве представления он будет использовать стандартный класс ListView , прилагаемый к ListFragment Создайте новый класс с именем NerdLauncherFragment и назначьте его суперклассом android.support.v4.app.ListFragment . Пока оставьте этот класс пустым. Откройте файл NerdLauncherActivity.java и измените суперкласс NerdLauncherActivity на SingleFragmentActivity . Удалите код шаблона и переопределите метод create- Fragment() так, чтобы он возвращал NerdLauncherFragment Листинг 23.1. Субкласс SingleFragmentActivity (NerdLauncherActivity.java) public class NerdLauncherActivity extends Activity SingleFragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_nerd_launcher); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_nerd_launcher, menu); return true; } @Override public Fragment createFragment() { return new NerdLauncherFragment(); }} Обработка неявного интента 385Обработка неявного интентаNerdLauncher отображает список приложений на устройстве. Для этого Nerd- Launcher отправляет неявный интент, на который должна отреагировать главная активность каждого приложения. В интент включается действие MAIN и категория LAUNCHER . Вы уже видели следующий фильтр интентов в своих проектах:
В файле NerdLauncherFragment.java переопределите метод onCreate(Bundle) для создания неявного интента. Получите от PackageManager список активностей, со- ответствующих интенту. Пока мы ограничимся простой регистрацией количества активностей, возвращенных PackageManager Листинг 23.2. Получение информации у PackageManager (NerdLauncherFragment.java) public class NerdLauncherFragment extends ListFragment { private static final String TAG = "NerdLauncherFragment"; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent startupIntent = new Intent(Intent.ACTION_MAIN); startupIntent.addCategory(Intent.CATEGORY_LAUNCHER); PackageManager pm = getActivity().getPackageManager(); List activities = pm.queryIntentActivities(startupIntent, 0); Log.i(TAG, "I've found " + activities.size() + " activities."); }} Запустите приложение NerdLauncher и посмотрите в данных LogCat, сколько при- ложений вернул экземпляр PackageManager В CriminalIntent для отправки отчетов использовался неявный интент. Чтобы представить на экране список выбора приложений, мы создали неявный интент, упаковали его в объект выбора и отправили ОС вызовом startActivity(Intent) : Intent i = new Intent(Intent.ACTION_SEND); ... // Создание и размещение дополнений интентов i = Intent.createChooser(i, getString(R.string.send_report)); startActivity(i); Почему мы не используем этот подход здесь? Вкратце дело в том, что фильтр интентов MAIN/LAUNCHER может соответствовать или не соответствовать неявному интенту MAIN/LAUNCHER , отправленному из startActivity(…) Оказывается, вызов startActivity(Intent) не означает «Запустить активность, соответствующую этому неявному интенту». Он означает «Запустить активность по умолчанию соответствующую этому неявному интенту». Когда вы отправляете |