Освой Android играючи

Сайт Александра Климова

Шкодим

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

ViewPager

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" />

Данные для отображения компонент берёт из адаптеров:

  • PagerAdapter (фрагменты не обязательны)
  • FragmentPagerAdapter (если используется мало фрагментов)
  • FragmentStatePagerAdapter (если много фрагментов)

Вам нужно унаследовать класс от нужного адаптера и реализовать свои методы. Добавление и удаление экранов реализуется с помощью методов instantiateItem() и destroyItem() соответственно. Элементы View для отображения можно создавать прямо в адаптере. Такой подход хорош тем, что ViewPager можно настраивать так, чтобы в адаптере не хранились все экраны сразу. По умолчанию адаптер хранит текущий экран, и по одному слева и справа от него. Это может сэкономить память, если содержание экранов слишком сложное.

Когда пользователь переключается на другой фрагмент, то вызывается метод onDestroyView(), а не onDestroy().

FragmentPagerAdapter

Рассмотрим простой пример с минимальным количеством кода для первого знакомства. Создадим новый проект. В 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

Если хочется добавить анимации при смене фрагментов, то воспользуйтесь специальным классом 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 заслуживает отдельной статьи.

PagerTitleStrip

Рассмотрим другой вариант и изучим другие возможности. Для примера будем использовать всего один фрагмент, содержимое которого будем менять динамически. А сверху добавим специальную полоску с заголовками.

Изменим разметку для фрагмента (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 – палец пользователя «доводит» страницу до конца.

Сейчас данный слушатель помечен как устаревший.

Адаптер FragmentStatePagerAdapter

Это старый вариант примера, когда компонент только появился. Я не стал делать ревизию, оставляю вам для самостоятельного изучения.

Адаптер 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

PageTransformer

Индикатор для ViewPager

Слайдер при помощи GridView и ViewPager

Знакомство с приложением

Devlight/InfiniteCycleViewPager: Infinite cycle ViewPager with two-way orientation and interactive effect

Реклама