Глава 31. Просмотр веб-страниц и WebView Листинг 31.4 (продолжение) @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_photo_page, parent, false); mWebView = (WebView)v.findViewById(R.id.webView); return v; } } Пока это всего лишь заготовка — вскоре мы заполним ее кодом. А пока создайте класс-контейнер PhotoPageActivity на основе хорошо знакомого класса Single- FragmentActivity Листинг 31.5. Создание веб-активности (PhotoPageActivity.java) package com.bignerdranch.android.photogallery; public class PhotoPageActivity extends SingleFragmentActivity { @Override public Fragment createFragment() { return new PhotoPageFragment(); } } Измените код PhotoGalleryFragment , чтобы вместо неявного интента происходило обращение к новой активности. Листинг 31.6. Переключение на обращение к новой активности (PhotoGalleryFragment.java) @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { mGridView.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView> gridView, View view, int pos, long id) { GalleryItem item = mItems.get(pos); Uri photoPageUri = Uri.parse(item.getPhotoPageUrl()); Intent i = new Intent(Intent.ACTION_VIEW, photoPageUri); Intent i = new Intent(getActivity(), PhotoPageActivity.class); i.setData(photoPageUri); startActivity(i); } }); return v; } Наконец, добавьте новую активность в манифест.
Простой способ: неявные интенты 513 Листинг 31.7. Добавление активности в манифест (AndroidManifest.xml) package="com.bignerdranch.android.photogallery" android:versionCode="1" android:versionName="1.0" > android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > android:launchMode="singleTop" android:label="@string/title_activity_photo_gallery" >
android:name=".PhotoPageActivity" />
Запустите приложение PhotoGallery и нажмите на фотографии. На экране появ- ляется новая пустая активность. Возьмемся за дело и заставим наш фрагмент делать что-то полезное. Чтобы виджет WebView успешно отображал страницу фотографии на сайте Flickr, необходимо выполнить три условия. Первое условие очевидно — нужно сообщить ему, какой URL-адрес необходимо загрузить. Второе условие — необходимо включить поддержку JavaScript. По умолчанию она отключена. Постоянно держать ее включенной не обязательно, но для Flickr она нужна. Android Lint выдает предупреждение (из-за потенциальной опасности меж- сайтовых сценарных атак), так что предупреждения Lint тоже нужно отключить. Наконец, необходимо переопределить в классе WebViewClient один метод с именем shouldOverrideUrlLoading(WebView,String) и вернуть false . Мы рассмотрим этот класс после того, как вы введете код. Листинг 31.8. Добавление новых переменных экземпляров (PhotoPageFragment.java) @SuppressLint("SetJavaScriptEnabled") @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_photo_page, parent, false); mWebView = (WebView)v.findViewById(R.id.webView); продолжение
514 Глава 31. Просмотр веб-страниц и WebView Листинг 31.8 (продолжение) mWebView.getSettings().setJavaScriptEnabled(true); mWebView.setWebViewClient(new WebViewClient() { public boolean shouldOverrideUrlLoading(WebView view, String url) { return false; } }); mWebView.loadUrl(mUrl); return v; } Загрузка данных с URL-адреса должна происходить после настройки WebView , по- этому она выполняется в последнюю очередь. До этого мы включаем JavaScript, вызывая getSettings() для получения экземпляра WebSettings , с последующим вызовом WebSettings.setJavaScriptEnabled(true) . Объект WebSettings — первый из трех механизмов настройки WebView . Он содержит различные свойства, которые можно задать в коде — например, строку пользовательского агента и размер текста. Далее выполняется настройка WebViewClient WebViewClient представляет собой событийный интерфейс. Предоставляя собственную реализацию WebViewClient , вы можете реагировать на события вывода. Например, можно определить, когда ядро визуализации начинает загрузку изображения с конкретного URL-адреса, или решить, стоит ли заново отправить серверу POST-запрос. WebViewClient содержит много методов, которые можно переопределить; большин- ство этих методов нам не понадобится. Однако мы должны заменить стандартную реализацию shouldOverrideUrlLoading(WebView,String) из WebViewClient . Этот метод указывает, что должно происходить при загрузке нового URL-адреса в WebView (например, при нажатии на ссылке). Если он возвращает true , это означает: «Не об- рабатывать этот URL-адрес, я обрабатываю его сам». Если он возвращает false , вы говорите: «Давай, WebView , загружай данные с URL-адреса, я с ним ничего не делаю». Реализация по умолчанию инициирует неявный интент с URL, по аналогии с тем, как это делалось ранее в этой главе. Для страницы с фотографией это создает се- рьезные проблемы, потому что Flickr первым делом выполняет перенаправление на мобильную версию сайта. Для WebViewClient это означает немедленное пере- ключение на браузер по умолчанию; совсем не то, что нам нужно. Проблема решается просто — переопределите реализацию по умолчанию, чтобы она возвращала false Запустите приложение PhotoGallery, и вы увидите WebView на экране. Класс WebChromeClient Раз уж мы занялись созданием собственной реализации WebView , давайте немного украсим ее, добавив представление заголовка и индикатор прогресса. Откройте файл fragment_photo_page.xml и внесите следующие изменения.
Простой способ: неявные интенты 515 Рис. 31.2. Добавление заголовка и индикатора прогресса (fragment_photo_page.xml) Добавить ProgressBar и TextView несложно, но чтобы подключить их, нам пона- добится вторая точка обратного вызова WebView : WebChromeClient . Если WebView- Client определяет интерфейс обработки событий визуализации, WebChromeClient определяет событийный интерфейс обработки событий, которые должны изменять элементы «хрома» (chrome) в браузере. К этой категории относятся сигналы (alerts) JavaScript, значки сайтов favicon, обновления прогресса загрузки и т. д. Подключите его в методе onCreateView(…)
516 Глава 31. Просмотр веб-страниц и WebView Листинг 31.9. Использование WebChromeClient (PhotoPageFragment.java) @SuppressLint("SetJavaScriptEnabled") @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_photo_page, parent, false); final ProgressBar progressBar = (ProgressBar)v.findViewById(R.id.progressBar); progressBar.setMax(100); // значения в диапазоне 0-100 final TextView titleTextView = (TextView)v.findViewById(R.id.titleTextView); mWebView = (WebView)v.findViewById(R.id.webView); mWebView.getSettings().setJavaScriptEnabled(true); mWebView.setWebViewClient(new WebViewClient() { }); mWebView.setWebChromeClient(new WebChromeClient() { public void onProgressChanged(WebView webView, int progress) { if (progress == 100) { progressBar.setVisibility(View.INVISIBLE); } else { progressBar.setVisibility(View.VISIBLE); progressBar.setProgress(progress); } } public void onReceivedTitle(WebView webView, String title) { titleTextView.setText(title); } }); mWebView.loadUrl(mUrl); return v; } Обновления индикатора прогресса и заголовка имеют собственные методы об- ратного вызова, onProgressChanged(WebView,int) и onReceivedTitle(WebView, String) . Информация о прогрессе, получаемая от onProgressChanged(WebView,int) , представляет собой целое число от 0 до 100. Если результат равен 100, значит, за- грузка страницы завершена, поэтому мы скрываем ProgressBar , задавая режим View.INVISIBLE Запустите приложение PhotoGallery и протестируйте внесенные изменения. Повороты в WebView Попробуйте повернуть экран. Хотя приложение работает правильно, вы замети- те, что WebView полностью перезагружает веб-страницу. Дело в том, что у WebView слишком много данных, чтобы сохранить их все в onSaveInstanceState(…) , и их
Для любознательных: внедрение объектов JavaScript 517 приходится создавать «с нуля» каждый раз, когда их приходится создавать заново при повороте. Для таких классов (другой пример — VideoView ) документация Android рекомендует позволить активности самой обработать все изменения конфигурации. Это означает, что вместо уничтожения активности она просто перемещает свои представления для размещения по новым размерам экрана. В результате WebView не приходится заново загружать свои данные. (Почему бы не делать так всегда, спросите вы? Такое решение не всегда работает правильно с любыми представлениями. Иначе жизнь разработчика была бы на- много проще, но… увы.) Чтобы заставить класс PhotoPageActivity обрабатывать свои изменения конфигу- рации, внесите следующее изменения в AndroidManifest.xml Листинг 31.10. Самостоятельный обработчик изменений конфигурации (AndroidManifest.xml) package="com.bignerdranch.android.photogallery" android:versionCode="1" android:versionName="1.0" > android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > android:configChanges="keyboardHidden|orientation|screenSize" />
Атрибут сообщает, что в случае изменения конфигурации из-за открытия или за- крытия клавиатуры, изменения ориентации или размеров экрана (которое также происходит при переключении между книжной и альбомной ориентацией в An- droid после версии 3.2) активность должна обрабатывать изменения самостоятельно. Вот и все. Попробуйте снова повернуть устройство; на этот раз все должно быть в ажуре. Для любознательных: внедрение объектов JavaScript Вы уже видели, как следует использовать WebViewClient и WebChromeClient для обработки некоторых событий, происходящих в WebView . Однако еще больше воз- можностей открывает внедрение произвольных объектов JavaScript в документ,
518 Глава 31. Просмотр веб-страниц и WebView содержащийся в виджете WebView . Откройте документацию по адресу https:// developer.android.com/reference/android/webkit/WebView.html и прокрутите до описания метода addJavascriptInterface(Object,String) . Этот метод позволяет внедрить произвольный объект в документ с заданным именем. mWebView.addJavascriptInterface(new Object() { public void send(String message) { Log.i(TAG, "Received message: " + message); } }, "androidObject"); После этого объект используется следующим образом:
Такое решение весьма рискованно — фактически вы разрешаете потенциально небезопасной веб-странице вмешиваться в работу вашей программы. Следователь- но, его стоит применять только для принадлежащей вам разметки HTML — или ограничиться предоставлением в высшей степени консервативного интерфейса.
Пользовательские представления и события касания В этой главе мы займемся обработкой событий касания. Для этого мы создадим субкласс View с именем BoxDrawingView . На этом представлении пользователь рисует прямоугольники, прикасаясь к экрану и перемещая палец. Рис. 32.1.Прямоугольники разных форм и размеров 32
520 Глава 32. Пользовательские представления и события касания Создание проекта DragAndDraw Класс BoxDrawingView занимает центральное место в новом проекте DragAndDraw. Выполните команду New Android Application Project . Задайте параметры проекта, как показано на рис. 32.2, и создайте пустую активность с именем DragAndDrawActivity Рис. 32.2. Создание проекта DragAndDraw Создание класса DragAndDrawActivity Класс DragAndDrawActivity представляет собой субкласс SingleFragmentActiv- ity , который заполняет обычный макет с одним фрагментом. На панели Package Explorer скопируйте файл SingleFragmentActivity.java в пакет com.bignerdranch.android. draganddraw . Затем скопируйте файл activity_fragment.xml в каталог res/layout проекта DragAndDraw. В файле DragAndDrawActivity.java объявите DragAndDrawActivity субклассом Single- FragmentActivity . Этот класс должен создавать экземпляр DragAndDrawFragment (класс, который будет создан следующим). Листинг 32.1. Изменение активности (DragAndDrawActivity.java) public class DragAndDrawActivity extends ActivitySingleFragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_drag_and_draw); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_drag_and_draw, menu); return true; }
Создание проекта DragAndDraw 521 @Override public Fragment createFragment() { return new DragAndDrawFragment(); } } Создание класса DragAndDrawFragment Чтобы подготовить макет DragAndDrawFragment , переименуйте файл activity_drag_ and_draw.xml в fragment_drag_and_draw.xml Макет DragAndDrawFragment в конечном итоге будет состоять из BoxDrawingView — пользовательского представления, которое мы собираемся написать. Весь графи- ческий вывод и обработка событий касания будут реализованы в BoxDrawingView Создайте новый класс с именем DragAndDrawFragment и назначьте его суперклассом android.support.v4.app.ListFragment . Переопределите метод onCreateView(…) , чтобы он заполнял макет fragment_drag_and_draw.xml Листинг 32.2. Создание фрагмента (DragAndDrawFragment.java) public class DragAndDrawFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_drag_and_draw, parent, false); return v; } } Запустите приложение DragAndDraw и убедитесь в том, что настройка была вы- полнена правильно. Рис. 32.3. DragAndDraw с макетом по умолчанию
522 Глава 32. Пользовательские представления и события касания Создание нестандартного представления Android предоставляет много превосходных стандартных представлений и вид- жетов, но иногда требуется создать нестандартное представление с визуальным оформлением, полностью уникальным для вашего приложения. Все многообразие нестандартных представлений можно условно разделить на две общие категории: простые представления — простое представление может быть устроено до- статочно сложно; «простым» оно называется только потому, что не имеет до- черних представлений. Простое представление почти всегда также выполняет нестандартную прорисовку; составные представления — состоят из других объектов представлений. Со- ставные представления обычно управляют дочерними представлениями, но не занимаются своей прорисовкой. Вместо этого каждому дочернему представле- нию делегируется своя часть работы по прорисовке. Создание нестандартного представления состоит из трех шагов: Выбор суперкласса. Для простого нестандартного представления View предо- ставляет пустой «холст» для рисования, поэтому этот выбор является наиболее распространенным. Для составных нестандартных представлений выберите подходящий класс макета. Субклассируйте выбранный класс и переопределите как минимум один кон- структор из суперкласса или создайте собственный конструктор, вызывающий один из конструкторов суперкласса. Переопределите другие ключевые методы для настройки поведения. Создание класса BoxDrawingView Класс BoxDrawingView относится к категории простых представлений и является прямым субклассом View Создайте новый класс с именем BoxDrawingView и назначьте View его суперклассом. Добавьте в файл BoxDrawingView.java два конструктора. Листинг 32.3. Исходная реализация BoxDrawingView (BoxDrawingView.java) public class BoxDrawingView extends View { // Используется при создании представления в коде public BoxDrawingView(Context context) { this(context, null); } // Используется при заполнении представления по разметке XML public BoxDrawingView(Context context, AttributeSet attrs) { super(context, attrs); } } Два конструктора нужны потому, что экземпляр представления может создаваться как в коде, так и по файлу разметки. Представления, созданные на базе файла макета,
Создание нестандартного представления 523 получают экземпляр AttributeSet с атрибутами XML, заданными в XML. Даже если вы не собираетесь использовать оба конструктора, их рекомендуется включить. Затем обновите файл макета fragment_drag_and_draw.xml , чтобы в нем использовалось новое представление. Листинг 32.4. Включение Add BoxDrawingView в макет (fragment_drag_and_draw.xml) xmlns:tools="https://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="@string/hello_world"/>
xmlns:android="https://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" /> Рис. 32.4. BoxDrawingView без прямоугольников