Освой Android играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
В предыдущем примере два фрагмента были полностью независимы друг от друга. Но в реальности такое не встречается. Фрагменты должны как-то общаться между собой. Поэтому пора переходить к следующей части - как взаимодействовать с фрагментами.
Чтобы было легче перестроиться на новую технологию, начнём издалека и создадим сначала следующую программу. Набросаем на экран несколько кнопок и других компонентов. Я по возможности оставляю старый вариант, чтобы не переписывать всю статью. Вы можете некоторые части кода менять на более современные аналоги, например, использовать ConstraintLayout.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".MainActivity" >
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Рыжик" />
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Барсик" />
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Мурзик" />
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Описание кота"
android:textAppearance="?android:attr/textAppearanceLarge" />
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:src="@drawable/cat_yellow" />
</LinearLayout>
Думаю, вам уже не составит труда написать код для кнопок, чтобы в нижней части экрана менялась картинка и текстовое содержание про каждого кота. Но я этого делать пока не буду.
Фрагмент, как и активность, состоит из разметки и класса. Сначала займёмся разметкой.
Логически экран можно разделить на две части - верхняя неизменяемая часть с кнопками и нижняя часть с текстовым блоком и контейнером для картинки, которая изменяет свой вид в зависимости от нажатой кнопки.
Создадим две отдельные разметки и скопируем нужные части из общей разметки в разметки для фрагментов. Делаем щелчок правой кнопкой мыши на папке res/layout и выбираем New | Layout Resource File.
Создаём новый файл fragment1.xml и размещаем верхнюю часть кода:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Рыжик" />
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Барсик" />
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Мурзик" />
</LinearLayout>
Также поступаем со вторым фрагментом - создаём новый файл fragment2.xml и в него копируем код из нижней части кода.
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Описание кота"
android:textAppearance="?android:attr/textAppearanceLarge" />
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="fitCenter"
android:src="@drawable/cat" />
</LinearLayout>
В обоих случаях мы вставляли код в корневой контейнер LinearLayout. Но если у вас была бы более сложная разметка с использованием контейнерных элементов, то вы могли бы копировать сразу готовый кусок кода без необходимости оборачивать его корневым элементом, как в нашем случае.
Пока мы создали разметки для будущих фрагментов. Теперь нужно создать отдельные классы для двух фрагментов. Для начала укажем, что наш класс должен наследоваться от класса Fragment. Не копируйте, а пишите код самостоятельно. Я создаю классы вручную с нуля, можно также воспользоваться готовым шаблоном Fragment (Blank), которым пользовались в первой части.
// Kotlin
import androidx.fragment.app.Fragment
class Fragment1: Fragment(){
}
// Java
public class Fragment1 extends Fragment {
}
Следите, чтобы импортировался класс androidx.fragment.app.Fragment, а не устаревшие классы.
Самостоятельно создайте класс для второго фрагмента Fragment2 по такому же принципу.
Настало время подключить разметки к фрагментам. В активностях мы подключали разметку в методе onCreate() через метод setContentView(). В фрагментах метод onCreate() служит для других задач. А для подключения разметки используется отдельный метод onCreateView().
Чтобы долго не искать нужный нам метод, просто вводите на клавиатуре первую и заглавные буквы метода - ocv. Такой комбинации соответствует только один метод, который нам и нужен. Нажимаем кнопку OK и в код фрагмента будет вставлен следующий шаблон:
// Kotlin
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return super.onCreateView(inflater, container, savedInstanceState)
}
// Java
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return super.onCreateView(inflater, container, savedInstanceState);
}
У метода используются три параметра. В первом параметре используется объект класса LayoutInflater, который позволяет построить нужный макет, считывая информацию из указанного XML-файла. Удалим строчку, которая возвращает результат и напишем свой вариант.
// Kotlin
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
//return super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.fragment1, container, false)
}
// Java
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
//return super.onCreateView(inflater, container, savedInstanceState);
View rootView =
inflater.inflate(R.layout.fragment1, container, false);
return rootView;
}
В Java-варианте код разбит на две части. Сначала мы получаем объект View, а затем уже его возвращаем в методе (не обязательно).
Скопируйте код метода onCreateView() и вставьте его в код класса Fragment2, не забыв указать разметку R.layout.fragment2.
Остальные два параметра container, false используются в связке и указывают на возможность подключения фрагментов в активность через контейнер динамически. Мы обойдёмся без динамики, а создадим собственные блоки для фрагментов, поэтому у нас используется значение false.
Возвращаемся к главной разметке активности. Смело удаляем все элементы с экрана, чтобы остался только корневой элемент LinearLayout.
В старых версиях студии на панели инструментов был готовый компонент <fragment>.
Сейчас компонент убрали, поэтому напишем код вручную в режиме Code.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".MainActivity">
<fragment
android:id="@+id/fragment1"
android:name="ru.alexanderklimov.fragment.Fragment1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:layout="@layout/fragment1" />
<fragment
android:id="@+id/fragment2"
android:name="ru.alexanderklimov.fragment.Fragment2"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
tools:layout="@layout/fragment2" />
</LinearLayout>
Обратите внимание на атрибут tools:layout="@layout/fragmentX" у тегов fragment, они помогут отобразить содержимое фрагментов в режиме дизайна.
Если сейчас запустим приложение, то тоже никаких изменений не увидим. Зачем тогда потратили столько времени на создание фрагментов? Непонятно.
А, я понял. Можно теперь писать в резюме про свои умения: использую фрагменты.
Однако, продолжим. Если повернуть устройство в альбомную ориентацию, то программа будет выглядеть не слишком красиво.
Мы знаем, что можно создать отдельную папку res/layout-land (перечитайте урок Ориентация) и разместить там разметку для такого случая.
Скопируем файл activity_main.xml и вставим его в новую папку.
Скорее всего вы не видите созданную папку, но она есть! Переключитесь в режим Project и заново откройте структуру проекта, найдя нужную папку. В дальнейшем оставайтесь в этом режиме для данного урока.
Изменим разметку фрагмента для альбомной ориентации.
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:baselineAligned="false"
android:orientation="horizontal"
tools:context=".MainActivity">
<fragment
android:id="@+id/fragment1"
android:name="ru.alexanderklimov.fragment.Fragment1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
tools:layout="@layout/fragment1" />
<fragment
android:id="@+id/fragment2"
android:name="ru.alexanderklimov.fragment.Fragment2"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
tools:layout="@layout/fragment2" />
</LinearLayout>
Не забывайте в имени фрагментов использовать свои названия пакетов. Совсем другое дело. Теперь в альбомной ориентации приложение выглядит намного лучше.
Но это мы могли сделать и без фрагментов. Зачем же они нужны? Пока версия с лишней строчкой в резюме остаётся основной - чтобы работодатель уважал за прогрессивный стиль.
Хотя небольшое удобство есть. Благодаря модульности, мы поменяли разметку только у фрагментов, а то, что было внутри фрагментов (кнопки, текстовые блоки и т.д.), мы не трогали.
Как уже говорилось, фрагменты были придуманы для того, чтобы обеспечить быстрое написание приложения под разные типы экранов - для смартфонов и планшетов. Часто бывает так, что на смартфоне на первом экране находится список, а когда пользователь нажимает на отдельный элемент списка, то запускается отдельная активность. А на планшете можно уместить список и дополнительные данные на одном экране, как можно увидеть на нашем последнем примере с альбомной ориентацией.
Давайте подключим поддержку планшетов. Создадим новую папку layout-sw600dp и скопируем в него файл из папки layout-land. Идентификатор sw600 говорит о минимальной ширине 600dp, что соответствует 7-дюймовым планшетам в альбомной ориентации. Существуют и другие варианты для планшетов с большими размерами.
Тут возникает небольшая проблема - если нам понадобится что-то изменить в разметке для альбомной ориентации, то придётся редактировать файлы во всех папках. Но есть выход из этой ситуации - использовать псевдонимы.
Мы можем создать одну копию разметки и указать, чтобы её использовали все нужные размеры устройств.
Делаем следующее. В папке layout-land переименовываем файл activity_main.xml в activity_main_wide.xml (Refactor | Rename) и перемещаем файл в папку layout. Пустую папку layout-land можно удалить.
Теперь создайте новую папку res/values-land. В созданной папке создаём новый файл refs.xml (имя не имеет значения, но так принято).
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="activity_main" type="layout">@layout/activity_main_wide
</item>
</resources>
Этот файл говорит, что в альбомной ориентации вместо ресурса activity_main следует подключать ресурс layout/activity_main_wide. Можете запустить приложение и убедиться, что ничего не изменилось.
Если у вас будет поддержка альбомных ориентаций для разных размеров планшетов, то просто копируйте файл refs.xml в папки типа values-720dp_land и др.
Теперь вы можете вносить изменения в одном файле activity_main_wide.xml, а не по отдельности в каждом файле.
В первой части мы узнали, что для создания фрагмента необходимо создать разметку, затем новый класс и в методе onCreateView() указать разметку. Затем в разметке активности указать тег fragment и присвоить ему имя класса фрагмента.
Поговорим о важном моменте. Вы можете установить связь между двумя фрагментами напрямую, чтобы при нажатии кнопки в первом фрагменте менялось содержимое во втором фрагменте. Но это неправильный подход, так как теряется смысл модульности фрагментов. Фрагменты ничего не должны знать о существовании друг друга. Любой фрагмент существует только в активности и только активность через свой специальный менеджер фрагментов должна управлять ими. А сами фрагменты должны реализовать необходимые интерфейсы, которые активность будет использовать в своих целях.
В первом фрагменте имеются кнопки. Добавим обработчик нажатий кнопок (такой же код вы могли использовать в активности, всё знакомо):
// Kotlin
class Fragment1: Fragment(), View.OnClickListener {
...
override fun onClick(v: View?) {
}
}
// Java
public class Fragment1 extends Fragment implements View.OnClickListener {
...
@Override
public void onClick(View view) {
}
}
Подключаем кнопки в методе onViewCreated. Код будет похож на код, который мы обычно используем в методе onCreate() у активности, только метод findViewById() будет относиться уже не к классу Activity (обычно, мы опускали это), а к корневому элементу разметки фрагмента, в нашем случае view/rootView. В Java-варианте используется старый пример до появления метода onViewCreated(). Раньше приходилось писать код в onCreateView().
// Kotlin
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val button1: Button = view.findViewById(R.id.button1)
val button2: Button = view.findViewById(R.id.button2)
val button3: Button = view.findViewById(R.id.button3)
button1.setOnClickListener(this)
button2.setOnClickListener(this)
button3.setOnClickListener(this)
}
// Java
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView =
inflater.inflate(R.layout.fragment1, container, false);
Button button1 = (Button) rootView.findViewById(R.id.button1);
Button button2 = (Button) rootView.findViewById(R.id.button2);
Button button3 = (Button) rootView.findViewById(R.id.button3);
button1.setOnClickListener(this);
button2.setOnClickListener(this);
button3.setOnClickListener(this);
return rootView;
}
Для начала просто выведем сообщение, что кнопка нажата.
// Kotlin
override fun onClick(v: View?) {
println("Вы нажали на кнопку")
}
// Java
@Override
public void onClick(View view) {
Toast.makeText(getActivity(), "Вы нажали на кнопку",
Toast.LENGTH_SHORT).show();
}
Запустите пример и проверьте. Но у нас три кнопки. Надо написать код, который бы получал информацию о нажатой кнопке, чтобы активность могла использовать эту информацию и использовать её для управления вторым фрагментом. Для удобства создадим в классе Fragment1 отдельный метод, который на основании идентификатора кнопки создаст нужный индекс:
// Kotlin
private fun translateIdToIndex(id: Int): Int {
var index = -1
when (id) {
R.id.button1 -> index = 1
R.id.button2 -> index = 2
R.id.button3 -> index = 3
}
return index
}
// Java
int translateIdToIndex(int id) {
int index = -1;
switch (id) {
case R.id.button1:
index = 1;
break;
case R.id.button2:
index = 2;
break;
case R.id.button3:
index = 3;
break;
}
return index;
}
Каждой кнопке соответствует свой индекс от 1 до 3.
Фрагмент всегда может узнать, в какой активности он находится, через метод getActivity(). В методе makeText() мы уже воспользовались данным методом, так как в фрагментах нет метода getApplicationContext().
Перепишем код для щелчка кнопки, чтобы узнать индекс нажатой кнопки.
// Kotlin
override fun onClick(v: View?) {
// Временный код для получения индекса нажатой кнопки
val buttonIndex = translateIdToIndex(v!!.id)
println("Вы щёлкнули по кнопке $buttonIndex")
}
// Java
@Override
public void onClick(View view) {
// Временный код для получения индекса нажатой кнопки
int buttonIndex = translateIdToIndex(view.getId());
Toast.makeText(getActivity(), String.valueOf(buttonIndex),
Toast.LENGTH_SHORT).show();
}
Теперь мы умеем определять индекс нажатой кнопки. Но пока эта информация доступна только самому фрагменту. Наша задача - передать эту информацию активности, которая затем передаст её другой активности.
Для этой цели используются интерфейсы.
Открываем код первого фрагмента Fragment1 и объявляем интерфейс с единственным методом до объявления самого класса Fragment1:
// Kotlin
interface OnSelectedButtonListener {
fun onButtonSelected(buttonIndex: Int)
}
// Java
public interface OnSelectedButtonListener {
void onButtonSelected(int buttonIndex);
}
Интерфейс не определяет работу метода, а только даёт ему имя. Класс, который будет использовать данный интерфейс, должен придумать, что делать в методе с данным именем.
У нас интерфейс будет использовать класс активности.
Переходим в класс активности и добавляем интерфейс OnSelectedButtonListener, который следует реализовать.
// Kotlin
class MainActivity : AppCompatActivity(), OnSelectedButtonListener {}
// Java
public class MainActivity extends AppCompatActivity
implements Fragment1.OnSelectedButtonListener {}
Среда разработки поможет создать заготовку для необходимого метода:
// Kotlin
override fun onButtonSelected(buttonIndex: Int) {
TODO("Not yet implemented")
}
// Java
@Override
public void onButtonSelected(int buttonIndex) {}
В этом методе надо написать такой код, чтобы активность получила индекс нажатой кнопки и передала информацию другому фрагменту, которая должна выполнить свою работу.
Но сначала подготовим второй фрагмент к работе. Объявим ссылки на компоненты, которые есть в разметке второго фрагмента. А также загрузим массив строк из ресурсов, который будем использовать для описания котов. Не забывайте, что в Java-варианте используется устаревший код.
// Kotlin
package ru.alexanderklimov.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.fragment.app.Fragment
class Fragment2 : Fragment() {
private lateinit var infoTextView: TextView
private lateinit var catImageView: ImageView
private lateinit var catDescriptions: Array<String>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment2, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
infoTextView = view.findViewById(R.id.textView)
catImageView = view.findViewById(R.id.imageView)
catDescriptions = resources.getStringArray(R.array.cats)
}
}
// Java
public class Fragment2 extends Fragment {
private TextView mInfoTextView;
private ImageView mCatImageView;
private String[] mCatDescriptionArray;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView =
inflater.inflate(R.layout.fragment2, container, false);
mInfoTextView = (TextView) rootView.findViewById(R.id.textView);
mCatImageView = (ImageView) rootView.findViewById(R.id.imageView);
// загружаем массив из ресурсов
mCatDescriptionArray = getResources().getStringArray(R.array.cats);
return rootView;
}
}
Массив задаём в ресурсах (файл res/values/strings.xml). Так как первый элемент массива идёт под индексом 0, то добавим нейтральный текст:
<string-array name="cats">
<item>Просто кот</item>
<item>Рыжик - рыжий кот</item>
<item>Барсик болеет за Барселону</item>
<item>Мурзик выписывает Мурзилку</item>
</string-array>
Подготовим метод, который будет менять содержимое фрагмента в зависимости от индекса нажатой кнопки:
// Kotlin
fun setDescription(buttonIndex: Int) {
val description: String = catDescriptions[buttonIndex]
infoTextView.text = description
when (buttonIndex) {
1 -> catImageView.setImageResource(R.drawable.cat1)
2 -> catImageView.setImageResource(R.drawable.cat2)
3 -> catImageView.setImageResource(R.drawable.cat3)
else -> {
}
}
}
// Java
public void setDescription(int buttonIndex) {
String catDescription = mCatDescriptionArray[buttonIndex];
mInfoTextView.setText(catDescription);
switch (buttonIndex) {
case 1:
mCatImageView.setImageResource(R.drawable.cat1);
break;
case 2:
mCatImageView.setImageResource(R.drawable.cat2);
break;
case 3:
mCatImageView.setImageResource(R.drawable.cat3);
break;
default:
break;
}
}
Осталось только получить информацию от активности (не от фрагмента) об индексе.
Опять возвращаемся в активность и напишем код для пустого метода onButtonSelected(), который будет получать от первого фрагмента индекс нажатой кнопки и передавать его второму фрагменту:
// Kotlin
override fun onButtonSelected(buttonIndex: Int) {
// подключаем FragmentManager
val fragmentManager = supportFragmentManager
// Получаем ссылку на второй фрагмент по ID
val fragment2 = fragmentManager.findFragmentById(R.id.fragment2) as Fragment2?
fragment2?.setDescription(buttonIndex)
}
// Java
@Override
public void onButtonSelected(int buttonIndex) {
// подключаем FragmentManager
FragmentManager fragmentManager = getSupportFragmentManager();
// Получаем ссылку на второй фрагмент по ID
Fragment2 fragment2 = (Fragment2) fragmentManager
.findFragmentById(R.id.fragment2);
// Выводим нужную информацию
if (fragment2 != null)
fragment2.setDescription(buttonIndex);
}
Активность получает доступ к своим фрагментам через специальный менеджер фрагментов (коты называют его манагером). Менеджер есть у любой активности, поэтому мы его не создаём через конструкцию new FragmentManager, а получаем через метод getSupportFragmentManager().
Менеджер фрагментов держит в руках все нити управления над своими фрагментами. Найти нужный фрагмент можно по идентификатору через метод FragmentManager.findFragmentById(), который похож на метод findViewById() для получения идентификатора кнопки, метки и т.д. У менеджера есть ещё один метод для поиска фрагмента по тегу findFragmentByTag().
В созданной заготовке вызываем менеджер фрагментов, получаем ссылку на второй фрагмент через его идентификатор и вызываем его метод setDescription().
Во втором фрагменте у нас нет надобности создавать интерфейс, так как фрагменту не нужно ничего сообщать активности. Он исполняет пассивную роль и ему нужно только получить данные для работы.
Опять возвращаемся к классу первого фрагмента. Сейчас нажатие кнопки приводит к появлению всплывающего сообщения об индексе кнопки.
Но теперь в методе onClick() мы можем получить доступ к слушателю активности.
// Kotlin
override fun onClick(v: View?) {
val buttonIndex = translateIdToIndex(v!!.id)
val listener = activity as OnSelectedButtonListener?
listener?.onButtonSelected(buttonIndex)
}
// Java
@Override
public void onClick(View view) {
int buttonIndex = translateIdToIndex(view.getId());
OnSelectedButtonListener listener = (OnSelectedButtonListener) getActivity();
listener.onButtonSelected(buttonIndex);
// Можно закомментировать
Toast.makeText(getActivity(), String.valueOf(buttonIndex),
Toast.LENGTH_SHORT).show();
}
По цепочке мы передаём информацию от первого фрагмента в активность, а затем активность передаёт информацию во второй фрагмент.
Если посмотреть на код двух фрагментов, то увидим, что они полностью независимы и не обращаются ни конкретно к друг другу, ни к определённой активности. Принцип модульности соблюдён. Вы можете добавить любой из этих фрагментов в любую новую активность и при этом вам не придётся менять код в самих фрагментах. Весь необходимый функционал в фрагментах уже прописан.
Запустите проект и проверьте на работоспособность. Для данного случая мы пока не получили никаких преимуществ в использовании фрагментов. Но сейчас главное для вас - понять основные принципы создания и взаимодействия фрагментов.
Спустя несколько лет возможности фрагментов расширились и теперь передавать данные можно проще.
Обсуждение статьи на форуме.