Глава 20. Камера II: Съемка и обработка изображений
Также нам понадобится виджет
ImageView в альбомном макете (рис. 20.9).
Рис. 20.9. Альбомный макет с ImageView (layout-land/fragment_crime.xml)
В файле
CrimeFragment.java определите новое поле и получите ссылку на
ImageView в методе onCreateView(…)
Листинг 20.11. Подготовка ImageView (CrimeFragment.java)
public class CrimeFragment extends Fragment {
private ImageButton mPhotoButton;
private ImageView mPhotoView;
@Override public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
mPhotoButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Launch the camera activity
Intent i = new Intent(getActivity(), CrimeCameraActivity.class);
startActivityForResult(i, REQUEST_PHOTO);
}
});
mPhotoView = (ImageView)v.findViewById(R.id.crime_imageView);
}
Чтобы убедиться в том, что виджет
ImageView находится на положенном месте, просмотрите макет или запустите приложение CriminalIntent.
Обновление уровня модели
345
Обработка изображения
Для вывода изображения на виджете
ImageView потребуется некоторая предвари- тельная обработка, потому что файлы, полученные с камеры, могут быть просто огромными. С каждым годом фирмы-производители устанавливают на своих теле- фонах все большие и современные камеры. Технический прогресс радует пользо- вателей, но создает проблемы программистам.
На момент написания книги самые современные телефоны на платформе Android оснащались 8-мегапиксельными камерами. Изображения такого размера мгновенно израсходуют всю доступную память, поэтому нам понадобится код, который будет масштабировать изображение перед загрузкой, а также код стирания ненужных изображений.
Добавление масштабированных фотографий
в ImageView
В пакете com.bignerdranch.android.criminalintent создайте новый класс с именем
PictureUtils
. Добавьте в файл
PictureUtils.java метод, масштабирующий изображение по размеру стандартного экрана устройства.
Листинг 20.12. Добавление класса PictureUtils (PictureUtils.java)
public class PictureUtils {
/**
* Получение объекта BitmapDrawable по данным локального файла,
* масштабированного по текущим размерам окна.
*/
@SuppressWarnings("deprecation")
public static BitmapDrawable getScaledDrawable(Activity a, String path) {
Display display = a.getWindowManager().getDefaultDisplay();
float destWidth = display.getWidth();
float destHeight = display.getHeight();
// Чтение размеров изображения на диске
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
float srcWidth = options.outWidth;
float srcHeight = options.outHeight;
int inSampleSize = 1;
if (srcHeight > destHeight || srcWidth > destWidth) {
if (srcWidth > srcHeight) {
inSampleSize = Math.round(srcHeight / destHeight);
} else {
inSampleSize = Math.round(srcWidth / destWidth);
}
}
options = new BitmapFactory.Options();
options.inSampleSize = inSampleSize;
продолжение
346
Глава 20. Камера II: Съемка и обработка изображений
Листинг 20.12 (продолжение)
Bitmap bitmap = BitmapFactory.decodeFile(path, options);
return new BitmapDrawable(a.getResources(), bitmap);
}
}
Методы
Display.getWidth()
и
Display.getHeight()
считаются устаревшими (dep- recated). Эта тема более подробно рассматривается в конце главы.
В идеале изображение стоило бы масштабировать так, чтобы оно точно соответ- ствовало размерам
ImageView
. Однако размер представления, в котором выводится представление, часто бывает недоступен в нужный момент. Например, в методе onCreateView(…)
мы не можем получить размер
ImageView
. Для надежности изо- бражение масштабируется по размерам текущего экрана устройства, который доступен всегда. Представление, в котором будет выводиться фотография, может быть меньше стандартного размера экрана, но больше быть не может.
Затем добавьте в
CrimeFragment закрытый метод, который связывает масштабиро- ванную версию изображения с
ImageView
Листинг 20.13. Добавление showPhoto() (CrimeFragment.java)
@Override public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
}
private void showPhoto() {
// Назначение изображения, полученного на основе фотографии
Photo p = mCrime.getPhoto();
BitmapDrawable b = null;
if (p != null) {
String path = getActivity()
.getFileStreamPath(p.getFilename()).getAbsolutePath();
b = PictureUtils.getScaledDrawable(getActivity(), path);
}
mPhotoView.setImageDrawable(b);
}
Включите в файл
CrimeFragment.java реализацию onStart()
с вызовом showPhoto()
, что- бы фотография была готова к тому моменту, как пользователь увидит
CrimeFragment
Листинг 20.14. Загрузка изображения (CrimeFragment.java)
private void showPhoto() {
// Назначение изображения, полученного на основе фотографии
Photo p = mCrime.getPhoto();
BitmapDrawable b = null;
if (p != null) {
String path = getActivity()
.getFileStreamPath(p.getFilename()).getAbsolutePath();
b = PictureUtils.getScaledDrawable(getActivity(), path);
}
Обновление уровня модели
347
mPhotoButton.setImageDrawable(b);
}
@Override
public void onStart() {
super.onStart();
showPhoto();
}
Включите в метод
CrimeFragment.onActivityResult(…)
вызов showPhoto()
, чтобы изображение было видимым при возвращении пользователя из
CrimeCameraActivity
Листинг 20.15. Вызов showPhoto() в onActivityResult(…) (CrimeFragment.java)
@Override public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) return;
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);
showPhoto();
Log.i(TAG, "Crime: " + mCrime.getTitle() + "has a photo");
}
}
}
Выгрузка изображения
Включите в класс
PictureUtils метод для уничтожения экземпляра
BitmapDrawable
, связанного с
ImageView
(если он существует).
Листинг 20.16. Очистка данных (PictureUtils.java)
public class PictureUtils {
/**
* ...
*/
@SuppressWarnings("deprecation")
public static BitmapDrawable getScaledDrawable(Activity a, String path) {
}
public static void cleanImageView(ImageView imageView) {
if (!(imageView.getDrawable() instanceof BitmapDrawable))
return;
// Стирание изображения для экономии памяти
BitmapDrawable b = (BitmapDrawable)imageView.getDrawable();
b.getBitmap().recycle();
imageView.setImageDrawable(null);
}
}
348Глава 20. Камера II: Съемка и обработка изображений
О вызове
Bitmap.recycle()
стоит рассказать подробнее. В
документации говорится, что вызывать этот метод не обязательно, но это не так.
Bitmap.recycle()
освобождает системную (native) память, занимаемую растровым изображением. Это большая часть содержания объекта растрового изображения. (Системная память может со- держать больший или меньший объем данных в зависимости от версии Android.
До Honeycomb в ней хранились все данные объектов Java
Bitmap
.)
Если не освободить память явным вызовом recycle()
, она в конечном итоге все равно будет освобождена. Однако освобождение произойдет когда-то в будущем в
завершителе (finalizer), а не при уничтожении самого изображения в ходе уборки мусора. Соответственно возникает вероятность исчерпания свободной памяти до вызова завершителя.
Момент выполнения завершителя заранее неизвестен, поэтому такие ошибки сложно отслеживать и воспроизводить. Таким образом, при большом размере изображений (как в нашем случае) лучше вызвать recycle()
для предотвращения неприятных ошибок памяти.
Включите в
CrimeFragment реализацию onStop()
с вызовом cleanImageView(…)
Листинг 20.17. Выгрузка изображения (CrimeFragment.java)
@Override public void onStart() {
super.onStart();
showPhoto();
}
@Overridepublic void onStop() { super.onStop(); PictureUtils.cleanImageView(mPhotoView);}Загрузка изображений в onStart()
с выгрузкой в onStop()
— полезная практика.
Эти методы отмечают точки, в которых активность может быть видна пользователю.
Если же выполнять загрузку и выгрузку в onResume()
и onPause()
, результат может оказаться неожиданным для пользователя.
Приостановленная активность все равно может быть частично видимой — если, например, поверх нее открывается активность, не занимающая весь экран. Если использовать onResume()
и onPause()
, изображения в таких ситуациях будут ис- чезать и появляться. Лучше загружать изображения, как только ваша активность становится доступной, и откладывать выгрузку до того момента, когда активность гарантированно не видна на экране.
Запустите приложение CriminalIntent. Сделайте снимок и убедитесь в том, что он появился в
ImageView
. Закройте CriminalIntent и запустите приложение заново.
Убедитесь в том, что при возвращении к тому же элементу списка преступлений фотография отображается, как и положено.
Ориентация
CrimeCameraActivity подсказывает пользователю, что фотографии следует делать в альбомной ориентации. Если фотография будет сделана в книж- ной ориентации, то изображение может быть неправильно расположено на кнопке.
Этот недостаток будет исправлен в первом упражнении.
Отображение полноразмерных изображений в DialogFragment
349
Отображение полноразмерных изображений
в DialogFragment
Наша работа над поддержкой камеры завершится отображением увеличенных версий фотографий
Crime
Рис. 20.10. DialogFragment с увеличенным изображением
Создайте новый класс в пакете com.bignerdranch.android.criminalintent
. При- свойте классу имя
ImageFragment
; назначьте его субклассом
DialogFragment
В аргументах фрагмента
ImageFragment должен передаваться путь к файлу фото- графии для
Crime
. В файле
ImageFragment.java добавьте метод newInstance(String)
, который получает путь к файлу и помещает его в пакет аргументов, как показано в листинге 20.18.
Листинг 20.18. Создание ImageFragment (ImageFragment.java)
public class ImageFragment extends DialogFragment {
public static final String EXTRA_IMAGE_PATH =
"com.bignerdranch.android.criminalintent.image_path";
public static ImageFragment newInstance(String imagePath) {
Bundle args = new Bundle();
продолжение
350
Глава 20. Камера II: Съемка и обработка изображений
Листинг 20.18 (продолжение)
args.putSerializable(EXTRA_IMAGE_PATH, imagePath);
ImageFragment fragment = new ImageFragment();
fragment.setArguments(args);
fragment.setStyle(DialogFragment.STYLE_NO_TITLE, 0);
return fragment;
}
}
Фрагменту назначается стиль
DialogFragment.STYLE_NO_TITLE
, с которым фрагмент имеет минималистское оформление, показанное на рис. 20.10.
ImageFragment не нужны заголовок и кнопки, предоставляемые
AlertDialog
. Если ваш фрагмент может обойтись без них, лучше использовать более элегантное, бы- строе и гибкое решение с переопределением onCreateView(…)
и использованием простого объекта
View
(вместо переопределения onCreateDialog(…)
и использова- ния Dialog).
В файле
ImageFragment.java переопределите метод onCreateView(…)
, чтобы он соз- давал объект
ImageView
«с нуля» с получением пути к файлу из своих аргументов.
Затем получите масштабированную версию изображения и свяжите ее с
ImageView
Также переопределите onDestroyView()
, который должен освобождать память, когда надобность в изображении отпадает.
Листинг 20.19. Создание ImageFragment (ImageFragment.java)
public class ImageFragment extends DialogFragment {
public static final String EXTRA_IMAGE_PATH =
"com.bignerdranch.android.criminalintent.image_path";
public static ImageFragment newInstance(String imagePath) {
}
private ImageView mImageView;
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup parent, Bundle savedInstanceState) {
mImageView = new ImageView(getActivity());
String path = (String)getArguments().getSerializable(EXTRA_IMAGE_PATH);
BitmapDrawable image = PictureUtils.getScaledDrawable(getActivity(), path);
mImageView.setImageDrawable(image);
return mImageView;
}
@Override
public void onDestroyView() {
super.onDestroyView();
PictureUtils.cleanImageView(mImageView);
}
}
Упражнение. Удаление фотографий
351
Наконец, необходимо вывести это диалоговое окно из
CrimeFragment
. В файле
CrimeFragment.java добавьте слушателя для mPhotoView
. В его реализации создайте экземпляр
ImageFragment и добавьте его в экземпляр
FragmentManager активности
CrimePagerActivity вызовом метода show(…)
для
ImageFragment
. Также понадобится строковая константа для идентификации
ImageFragment в
FragmentManager
Листинг 20.20. Отображение ImageFragment (CrimeFragment.java)
public class CrimeFragment extends Fragment {
private static final String DIALOG_IMAGE = "image";
@Override
@TargetApi(11)
public View onCreateView(LayoutInflater inflater, ViewGroup parent,
Bundle savedInstanceState) {
mPhotoView = (ImageView)v.findViewById(R.id.crime_imageView);
mPhotoView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Photo p = mCrime.getPhoto();
if (p == null)
return;
FragmentManager fm = getActivity()
.getSupportFragmentManager();
String path = getActivity()
.getFileStreamPath(p.getFilename()).getAbsolutePath();
ImageFragment.newInstance(path)
.show(fm, DIALOG_IMAGE);
}
});
}
Запустите приложение CriminalIntent. Сделайте снимок и убедитесь в том, что теперь приложение позволяет просмотреть фотографию сцены преступления во всех подробностях.
Упражнение. Ориентация изображения в Crime
Иногда пользователь хочет сделать снимок в книжной ориентации. Найдите в до- кументации API информацию о том, как обнаружить текущую ориентацию. Сохра- ните правильную ориентацию в
Photo и используйте ее для поворота изображения в
CrimeFragment и
ImageFragment
Упражнение. Удаление фотографий
В текущей версии приложения можно заменить фотографию
Crime
, но при этом ста- рый файл остается и занимает место на диске. Добавьте в метод onActivityResult(int,
352
Глава 20. Камера II: Съемка и обработка изображений int,
Intent)
класса
CrimeFragment код проверки существующей фотографии и уда- ления соответствующего файла с диска.
В качестве дополнительного задания предоставьте пользователю возможность удаления фотографии без замены. Реализуйте в
CrimeFragment контекстное меню и/или режим контекстных действий, активизируемый долгим нажатием на ми- ниатюре изображения. Команда меню
Delete
Photo удаляет фотографию с диска, из модели и
ImageView
Для любознательных: устаревшие
конструкции в Android
В главе 19, при назначении размера области предварительного просмотра камеры мы использовали устаревший метод и устаревшую константу. В этой главе тоже ис- пользовались устаревшие методы. У наблюдательного читателя может возникнуть вопрос: «Что это вообще значит?»
Начнем с того, что же понимать под самим термином. Если некоторая часть API считается устаревшей, это означает, что она более не является необходимой. Иногда устаревание происходит из-за того, что выполняемая операция стала ненужной — как в случае с методом
SurfaceHolder.setType(int)
и константой
SurfaceHolder.
SURFACE_TYPE_PUSH_BUFFERS
, использованными в главе 19.
В старых версиях Android экземпляр
SurfaceHolder должен был настраиваться в соответствии с предполагаемым использованием. Сейчас это уже не нужно, по- этому метод setType(…)
стал бесполезным.
В других случаях это означает, что метод был заменен новым методом, который по какой-то причине является предпочтительным. Например, класс
BitmapDrawable содержит устаревший конструктор
BitmapDrawable(Bitmap)
, при использовании которого часто допускались ошибки. А может быть, новый метод «чище» с точки зрения архитектуры, как, например, метод
View.setBackgroundDrawable(Drawable)
Также можно вспомнить методы
Display.getWidth()
и
Display.getHeight()
, кото- рые использовались ранее. Сейчас они заменены одним методом getSize(Point)
, который предотвращает ошибки, возникавшие при последовательном вызове get-
Width()
и getHeight()
Проблема устаревания кода решается по-разному в зависимости от того, на какой платформе вы работаете. Две крайности воплощены в двух подходах, которые вам, вероятно, знакомы — подходе Microsoft и подходе Apple.
В подходе Microsoft части API устаревают, но никогда не удаляются. Это связано с тем, что в мировоззрении Microsoft важнейшим фактором является работоспо- собность наибольшего количества программ в любой версии ОС. Когда Microsoft представляет общедоступный API, он всегда поддерживается. Порой доходит до сохранения ошибочного, недокументированного поведения для обеспечения обрат- ной совместимости. Из-за этого мир Microsoft порой выглядит довольно странно.
С другой стороны, в мире Apple API удаляются из ОС вскоре после того, как они были объявлены устаревшими. Руководители мира Apple стремятся иметь чистую,
Для любознательных: устаревшие конструкции в Android
353
красивую ОС. Их не волнует, сколько API придется убить для достижения этой цели. В результате мир Apple очень чист и аккуратен, но старые программы часто перестают работать без активного обновления.
В мире Apple для поддержки как старых, так и новых операционных систем при- ходится использовать конструкции следующего вида:
float destWidth;
float destHeight;
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB_MR2) {
Point size = display.getSize();
destWidth = size.x;
destHeight = size.y;
} else {
destWidth = display.getWidth();
destHeight = display.getHeight();
}
Дело в том, что в мире Apple методы getWidth()
и getHeight()
, вероятно, скоро исчезнут. Нужно действовать осторожно, чтобы случайно не вызвать несуществу- ющий метод.
Нельзя сказать. что идеология Android совпадает с подходом Microsoft, но по крайней мере достаточно близка к нему. Каждая версия Android SDK в основном сохраняет обратную совместимость с предыдущей версией, а это означает, что методы API почти никогда не удаляются. Значит, вам не нужно избегать вызова старых методов. В книге мы будем использовать устаревшие методы там, где это необходимо, и будем подавлять предупреждения при помощи аннотаций. И все же старайтесь не использовать устаревшие API без необходимости, чтобы ваш код оставался стильным и элегантным.
Неявные интентыВ Android можно запустить активность из другого приложения на устройстве при помощи
неявного интента (implicit intent). В явном интенте задается класс запускаемой активности, а ОС запускает его. В неявном интенте вы описываете операцию,
которую необходимо выполнить, а ОС запускает активность соответ- ствующего приложения.
В приложении CriminalIntent мы будем использовать неявные интенты для выбора подозреваемых из списка контактов пользователя и отправки текстовых отчетов о преступлении. Пользователь выбирает подозреваемого в контактном приложении, установленном на устройстве, и получает список приложений для отправки отчета.
Открыть приложение, которое умеет отправлять отчеты
Открыть приложение для работы с контактами
Рис. 21.1. Открытие приложений для выбора контактов и отправки отчетов
21
Добавление кнопок
355
Использовать функциональность других приложений при помощи неявных ин- тентов намного проще, чем писать собственные реализации стандартных задач.
Пользователям также нравится работать с приложениями, которые им уже хорошо знакомы, в сочетании с вашим приложением.
Прежде чем создавать неявные интенты, необходимо выполнить с CriminalIntent ряд подготовительных действий:
добавить в макеты
CrimeFragment кнопки выбора подозреваемого и отправки отчета;
добавить в класс
Crime поле mSuspect
, в котором будет храниться имя подо- зреваемого;
создать отчет о преступлении с использованием форматных строк ресурсов.
Добавление кнопок
Начнем с включения в макеты
CrimeFragment новых кнопок. Прежде всего добавьте строки, которые будут отображаться на кнопках.
Листинг 21.1. Добавление строк для надписей на кнопках (strings.xml)
Take!
Choose Suspect
Send Crime Report
Добавьте в файл layout/fragment_crime.xml два виджета
Button
, представленных на рис. 21.2. Обратите внимание: на диаграмме не показан первый виджет
LinearLayout
, чтобы вы могли сосредоточиться на новых и интересных частях диаграммы.
Рис. 21.2. Добавление кнопок для выбора контактов и отправки отчетов
(layout/fragment_crime.xml)
В альбомном макете мы назначим новые кнопки потомками нового горизон- тального виджета
LinearLayout
, расположенного под виджетом с кнопкой даты и флажком.