Освой Android играючи
/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000
FragmentPagerAdapter
PageTransformer
PagerTitleStrip
FragmentStatePagerAdapter
В Android есть компонент ViewFlipper, который позволяет прокручивать экран влево-вправо. Но он имеет ряд недостатков. ViewFlipper не позволяет с лёгкостью добиться эффекта привязки, когда экраны двигаются вместе с пальцем. Также нужен механизм автоматической доводки экранов в ту или иную сторону.
Google предложила другой вариант - использовать компонент ViewPager, который входит в состав Support Package. В новых проектах необходимая библиотека добавляется автоматически, поэтому мы сразу можем попробовать написать демонстрационный пример.
Так как компонент относится к пакету совместимости, то используемые фрагменты должны относиться к классу android.support.v4.app.Fragment, даже если вы собираетесь писать приложение для новых устройств, которые поддерживают стандартные фрагменты. На данный момент библиотека переписывается на AndroidX, поэтому я буду по возможности заменять старые примеры. Все названия классов и методов остаются, меняется только имя пакета.
ViewPager относится к категории ViewGroup и схож по работе с компонентами на основе AdapterView (ListView и Gallery). На панели инструментов Android Studio компонент можно найти в разделе Containers. При использовании ViewPager в разметке используйте полное имя класса. Старый вариант.
<android.support.v4.view.ViewPager
android:layout_width="match_parent"
android:layout_height="match_parent"
… />
Новый вариант.
<?xml version="1.0" encoding="utf-8"?>
<androidx.viewpager.widget.ViewPager
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Данные для отображения компонент берёт из адаптеров:
Вам нужно унаследовать класс от нужного адаптера и реализовать свои методы. Добавление и удаление экранов реализуется с помощью методов instantiateItem() и destroyItem() соответственно. Элементы View для отображения можно создавать прямо в адаптере. Такой подход хорош тем, что ViewPager можно настраивать так, чтобы в адаптере не хранились все экраны сразу. По умолчанию адаптер хранит текущий экран, и по одному слева и справа от него. Это может сэкономить память, если содержание экранов слишком сложное.
Когда пользователь переключается на другой фрагмент, то вызывается метод onDestroyView(), а не onDestroy().
Рассмотрим простой пример с минимальным количеством кода для первого знакомства. Создадим новый проект. В activity_main.xml добавим ViewPager из примера выше.
Подготовим разметки для фрагментов.
Сначала создадим ещё один файл разметки res/layout/fragment_1.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/detailsText"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal|center_vertical"
android:layout_marginTop="20dip"
android:text="Первый фрагмент"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textSize="30sp" />
</LinearLayout>
И ещё один файл res/layout/fragment_2.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" >
<ImageView
android:id="@+id/imageView1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@mipmap/ic_launcher" />
</LinearLayout>
Создадим новый класс для первого фрагмента Fragment1:
package ru.alexanderklimov.viewpager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class Fragment1 extends Fragment {
public Fragment1() {
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_1, container, false);
TextView textView = view.findViewById(R.id.detailsText);
textView.setText("Проверка");
return view;
}
}
Класс для второго фрагмента Fragment2:
package ru.alexanderklimov.viewpager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class Fragment2 extends Fragment {
public Fragment2() {
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_2, container, false);
}
}
Фрагменты подготовлены. Теперь в основном классе MainActivity создадим адаптер на основе FragmentPagerAdapter и напишем остальной код:
package ru.alexanderklimov.viewpager;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyAdapter adapter = new MyAdapter(getSupportFragmentManager());
ViewPager viewPager = findViewById(R.id.viewpager);
viewPager.setAdapter(adapter); // устанавливаем адаптер
viewPager.setCurrentItem(1); // выводим второй экран
}
public static class MyAdapter extends FragmentPagerAdapter {
MyAdapter(@NonNull FragmentManager fm) {
super(fm);
}
@Override
public int getCount() {
return 3;
}
@NonNull
@Override
public Fragment getItem(int position) {
switch (position) {
case 0:
return new Fragment1();
case 1:
return new Fragment2();
case 2:
return new Fragment2();
default:
return new Fragment1();
}
}
}
}
В методе getCount() следует возвращать количество страниц. В методе getItem() по номеру позиции нужно вернуть фрагмент.
Если у нас имеется три экрана, то может быть удобно сразу показать второй экран, чтобы можно было прокручивать или влево к первому экрану или вправо к третьему экрану. Это достигается вызовом метода setCurrentItem(1).
В нашем примере мне было лень создавать третий фрагмент, поэтому второй фрагмент используется два раза. В качестве фрагмента по умолчанию я установил первый фрагмент. Запустите пример и подвигайте экраны приложения вправо-влево.
Сам принцип теперь понятен? Нужно подготовить фрагменты и через адаптер сменять их.
Если хочется добавить анимации при смене фрагментов, то воспользуйтесь специальным классом PageTransformer. Попробуем менять прозрачность фрагмента.
...
viewPager.setCurrentItem(1); // выводим второй экран
viewPager.setPageTransformer(false, new ViewPager.PageTransformer() {
@Override
public void transformPage(@NonNull View v, float pos) {
final float opacity = Math.abs(Math.abs(pos) - 1);
v.setAlpha(opacity);
}
});
Класс PageTransformer заслуживает отдельной статьи.
Рассмотрим другой вариант и изучим другие возможности. Для примера будем использовать всего один фрагмент, содержимое которого будем менять динамически. А сверху добавим специальную полоску с заголовками.
Изменим разметку для фрагмента (fragment_1.xml):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/LinearLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical"
tools:context=".CatFragment">
<ImageView
android:id="@+id/topImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/catTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="Имя кота"
android:textAppearance="?android:attr/textAppearanceMedium" />
<TextView
android:id="@+id/catDescription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="Описание кота"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
Создадим два строковых массива в ресурсах (res/values/strings.xml):
<string-array name="catsTitles">
<item>Васька</item>
<item>Барсик</item>
<item>Мурзик</item>
<item>Рыжик</item>
</string-array>
<string-array name="catDescriptions">
<item>Васька слушает да ест</item>
<item>Барсик хочет в Барселону</item>
<item>Мурзик выписал Мурзилку</item>
<item>Рыжик рычит на рыбок</item>
</string-array>
Теперь подготовим класс для фрагмента CatFragment:
package ru.alexanderklimov.viewpager;
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.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
public class CatFragment extends Fragment {
static final String CAT_NAMES = "cats_names";
static final String CAT_DESCRIPTIONS = "cat_descriptions";
final static String TOP_IMAGE = "top image";
public CatFragment() {
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_1, container, false);
Bundle arguments = getArguments();
if (arguments != null) {
String catName = arguments.getString(CAT_NAMES);
String catDescription = arguments.getString(CAT_DESCRIPTIONS);
int topCardResourceId = arguments.getInt(TOP_IMAGE);
displayValues(view, catName, catDescription, topCardResourceId);
}
return view;
}
private void displayValues(View v, String name,
String catDescription, int topCardResourceId) {
TextView catNameTextView = v.findViewById(R.id.catTitle);
TextView catDescriptionTextView = v.findViewById(R.id.catDescription);
ImageView cardImageView = v.findViewById(R.id.topImage);
catNameTextView.setText(name);
catDescriptionTextView.setText(catDescription);
cardImageView.setImageResource(topCardResourceId);
}
}
При создании фрагмент извлекает данные из объекта Bundle и использует их для заполнения текстов и картинки.
Создадим класс для адаптера в отдельном файле для удобства с помощью мастера создания класса, указав в качестве суперкласса FragmentPagerAdapter.
package ru.alexanderklimov.viewpager;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
public class CatPagerAdapter extends FragmentPagerAdapter {
public CatPagerAdapter(@NonNull FragmentManager fm) {
super(fm);
}
@NonNull
@Override
public Fragment getItem(int position) {
return null;
}
@Override
public int getCount() {
return 0;
}
}
У адаптера должен быть пустой конструктор и два обязательных метода getItem() и getCount(). Немного переделаем конструктор, чтобы он использовал контекст, необходимый для извлечения данных из ресурсов.
import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
public class CatPagerAdapter extends FragmentPagerAdapter {
private String[] mCatNames;
private String[] mCatDescriptions;
private final int[] mTopImageResourceIds = { R.drawable.cat_gold,
R.drawable.cat_green, R.drawable.cat_white, R.drawable.cat_yellow };
CatPagerAdapter(FragmentManager fm, Context context) {
super(fm);
Resources resources = context.getResources();
mCatNames = resources.getStringArray(R.array.catsTitles);
mCatDescriptions = resources.getStringArray(R.array.catDescriptions);
}
@NonNull
@Override
public Fragment getItem(int position) {
Bundle arguments = new Bundle();
arguments.putString(CatFragment.CAT_NAMES, mCatNames[position]);
arguments.putString(CatFragment.CAT_DESCRIPTIONS,
mCatDescriptions[position]);
arguments.putInt(CatFragment.TOP_IMAGE, mTopImageResourceIds[position]);
CatFragment catsFragment = new CatFragment();
catsFragment.setArguments(arguments);
return catsFragment;
}
@Override
public int getCount() {
return mCatNames.length;
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return mCatNames[position];
}
}
Для подсчёта количества экранов используется свойство массива length. А для формирования содержимого экрана мы помещаем нужные данные в объект Bundle, которые затем будут переданы фрагменту. Данные находятся в трёх массивах: короткий текст, длинный текст и ссылка на изображение в ресурсах.
Метод getPageTitle() позволяет задать заголовок для фрагмента. Сам заголовок мы зададим в следующем шаге.
Приготовления почти закончены. Изменим разметку главной активности, чтобы у отдельного экрана появились заголовки. Для этого добавляется тег PagerTitleStrip:
<?xml version="1.0" encoding="utf-8"?>
<androidx.viewpager.widget.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager.widget.PagerTitleStrip
android:id="@+id/pager_title_strip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="#33b5e5"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:textColor="#fff" />
</androidx.viewpager.widget.ViewPager>
Если вы хотите видеть заголовок внизу фрагмента, то используйте android:layout_gravity="bottom".
Загружаем ViewPager и подключаем адаптер.
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.viewpager.widget.ViewPager;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
CatPagerAdapter catPagerAdapter = new CatPagerAdapter(getSupportFragmentManager(), this);
ViewPager viewPager = findViewById(R.id.viewpager);
viewPager.setAdapter(catPagerAdapter);
}
}
Пример на планшете в альбомной ориентации:
Свойства PagerTitleStrip можно изменить программно, например, поменять размер и цвет заголовков.
...
viewPager.setAdapter(catPagerAdapter);
PagerTitleStrip pagerTitleStrip = findViewById(R.id.pager_title_strip);
pagerTitleStrip.setTextSize(TypedValue.COMPLEX_UNIT_SP, 24);
pagerTitleStrip.setTextColor(Color.GREEN);
Тег PagerTitleStrip можно заменить на PagerTabStrip. В этом случае заголовке станут ещё и кнопками-вкладками. Ничего переписывать не придётся. Вы по-прежнему можете перелистывать страницы пальцами или нажимать на заголовки.
У ViewPager есть обработчик событий OnPageChangeListener с тремя методами:
viewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
Toast.makeText(getApplicationContext(), "Выбран " + position,
Toast.LENGTH_LONG).show();
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
Например, метод onPageSelected() позволяет получить номер текущего экрана при пролистывании (отсчёт от 0).
Метод onPageScrolled() даёт представление о текущем значении скрола при пролистывании.
Метод onPageScrollStateChanged() позволяет узнать о состоянии, в котором находится скрол (SCROLL_STATE_IDLE – экраны не листаются, SCROLL_STATE_DRAGGING – пользователь «тащит» страницу, SCROLL_STATE_SETTLING – палец пользователя «доводит» страницу до конца.
Сейчас данный слушатель помечен как устаревший.
Это старый вариант примера, когда компонент только появился. Я не стал делать ревизию, оставляю вам для самостоятельного изучения.
Адаптер FragmentPagerAdapter держит все фрагменты в памяти, а FragmentStatePagerAdapter создаёт их заново по мере необходимости. В этом их принципиальное отличие. Напишем пример и добавим поддержку кнопки Back.
Создадим разметку для отдельной страницы (pager.xml):
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView style="?android:textAppearanceMedium"
android:padding="16dp"
android:lineSpacingMultiplier="1.2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Здесь длинный текст с прокруткой" />
</ScrollView>
Создадим класс фрагмента с минимальным кодом
package ru.alexanderklimov.test;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
public class SlideFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// TODO Auto-generated method stub
ViewGroup rootView = (ViewGroup) inflater.inflate(
R.layout.pager, container, false);
return rootView;
}
}
Разметку активности оставим минимальной:
<android.support.v4.view.ViewPager xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Код для активности. Тут всё уже знакомо.
package ru.alexanderklimov.test;
import ...
public class MainActivity extends FragmentActivity {
// Число страниц
private static final int NUM_PAGES = 5;
// ViewPager
private ViewPager mViewPager;
// Адаптер
private PagerAdapter mPagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mViewPager = (ViewPager) findViewById(R.id.pager);
mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager());
mViewPager.setAdapter(mPagerAdapter);
}
@Override
public void onBackPressed() {
if (mViewPager.getCurrentItem() == 0) {
// If the user is currently looking at the first step, allow the
// system to handle the
// Back button. This calls finish() on this activity and pops the
// back stack.
super.onBackPressed();
} else {
// Otherwise, select the previous step.
mViewPager.setCurrentItem(mViewPager.getCurrentItem() - 1);
}
}
private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter {
public ScreenSlidePagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return new SlideFragment();
}
@Override
public int getCount() {
return NUM_PAGES;
}
}
}
В Android Studio в некоторых шаблонах встречается реализация ViewPager из коробки. Недавно Гугл представила бета-версию ViewPager2, которая должна заменить старый компонент.
ViewPager с адаптером PagerAdapter
Слайдер при помощи GridView и ViewPager