Когда я начинал изучать андроид, мне очень хотелось добавить красивое перелистывание экранов — как в популярных приложениях. Хотелось просто некоторое подобие LinearLayout, содержимое которого можно будет прокручивать. Однако для того, чтобы написать простейший слайдер с использованием ViewPager, мне потребовалось несколько часов на разбор, а также целая страница кода. К тому же, присутствовали некоторые ограничения — добавление объекта внутрь тега <ViewPager> не добавляло его в список виджетов.
Разобравшись, решил написать свой класс ScreenPager, который наследуется от ViewPager и работает очень просто. Причём работает как в xml, так и в чистом коде.
Использование
Для начала создайте простейшее приложение. Пусть его класс MainActivity находится в com.example.testapp и имеет метод onCreate. Теперь создайте в том же каталоге класс ScreenPager и добавьте туда следующий код, не вникая в него пока что:
package com.example.testapp;
import android.content.Context;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
public class ScreenPager extends ViewPager {
private PagerAdapter adapter = new ScreenPagerAdapter();
private List<View> viewList = new ArrayList<View>();
public ScreenPager(Context context) {
super(context);
init();
}
public ScreenPager(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
private void init() {
setAdapter(adapter);
}
@Override
public void addView(View child, ViewGroup.LayoutParams params) {
addScreen(child);
}
public void addScreen(View screen) {
viewList.add(screen);
adapter.notifyDataSetChanged();
}
private class ScreenPagerAdapter extends PagerAdapter {
@Override
public int getCount() {
return viewList.size();
}
@Override
public boolean isViewFromObject(View view, Object o) {
return view.equals(o);
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View view = viewList.get(position);
container.addView(view);
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
}
/**
* Use - in MainActivity.onCreate add:
* LayoutInflater.from(this).setFactory(ScreenPager.getShortNameFactory());
*
* @return
*/
static LayoutInflater.Factory getShortNameFactory() {
return new LayoutInflater.Factory() {
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
if (ScreenPager.class.getSimpleName().equals(name)) {
return new ScreenPager(context, attrs);
}
return null;
}
};
}
}
Теперь вы можете создать файл screenpager.xml в вашем res/layout и поместить туда следующий код
<?xml version="1.0" encoding="utf-8"?>
<com.example.testapp.ScreenPager
xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="match_parent"
a:layout_height="match_parent">
<Button a:text="Кнопка № 1" />
<Button a:text="Кнопка № 2" />
<Button a:text="Кнопка № 3" />
<Button a:text="Кнопка № 4" />
</com.example.testapp.ScreenPager>
Если вы вручную создаёте файл — то не забудьте сохранить его в кодировке UTF-8. И не забывайте о том, что строки нужно сохранять в строковых ресурсах, а не вставлять напрямую. В данной статье я намеренно не делал это, чтобы упростить код.
Теперь в MainActivity.onCreate добавим одну строчку
setContentView(R.layout.screenpager);
Всё! Слайдер работает. Но длинное имя com.example.testapp — это некрасиво. Мы же хотим сделать всё максимально просто, не так ли?
Оказалось, что система позволяет использовать короткие имена только для классов из android.view или android.widget. Если же мы хотим сделать так для нашего класса — следует переопределить Factory для LayoutInflater. Я написал метод getShortNameFactory, который возвращает специальную фабрику, позволяющую использовать сокращённое имя для нашего класса. Установим её в методе onCreate, причём сделать это надо до setContentView:
LayoutInflater.from(this).setFactory(ScreenPager.getShortNameFactory());
Также импортируйте LayoutInflater, если ваша IDE не сделает это за вас:
import android.view.LayoutInflater;
Теперь мы можем использовать использовать сокращённое название нашего класса:
<ScreenPager
xmlns:a="http://schemas.android.com/apk/res/android"
a:layout_width="match_parent"
a:layout_height="match_parent">
<Button a:text="Кнопка № 1" />
<Button a:text="Кнопка № 2" />
<Button a:text="Кнопка № 3" />
<Button a:text="Кнопка № 4" />
</ScreenPager>
Но это ещё не всё! Вы можете строить ваши экраны в коде, и это тоже просто:
ScreenPager screenPager = new ScreenPager(this);
Button button1 = new Button(this);
button1.setText("Кнопка №1");
screenPager.addScreen(button1);
Button button2 = new Button(this);
button2.setText("Кнопка №2");
screenPager.addScreen(button2);
//...
setContentView(screenPager);
Используйте на здоровье. Вместо кнопок можете вставлять ваши менеджеры компоновки или любые другие is-a View объекты. Правда ваша среда разработки (в моём случае это Android Studio) может ругаться на отсутствие layout_width и layout_height, но это предупреждение можно легко отключить.
Выкладываю полный исходный код примера.
Подробности
Как работает ViewPager и PagerAdapter, можете почитать в этой статье. Собственно, я по ней их и изучал.
Класс ScreenPager наследуется от ViewPager и включает в себя нестатический вложенный класс — ScreenPagerAdapter, который наследуется от PagerAdapter. Если понадобится его получить — это можно будет сделать через getAdapter(). Также внутри ScreenPager находится список из View, с которыми он должен работать.
Когда обрабатывается xml-код, происходит вызов addView для каждого объекта внутри тега ScreenPager. Мы переопределили эту функцию и она перенаправляет View в addScreen, чтобы занести его в коллекцию и уведомить адаптер об этом. Таким образом, оба случая — и создание слайдера через код, и через xml — проходят через addScreen.
Далее, я добавил небольшую статическую функцию, которая возвращает LayoutInflater.Factory. Мы просто проверяем короткое имя нашего класса — насколько оно совпадает с именем тега — и в случае успеха возвращаем наш объект.
А теперь самое интересное. Вы наверно замечали, что, к примеру, LinearLayout заставляет для всех дочерних объектов указывать layout_width и layout_height. А GridLayout — не требует этого. Мне очень захотелось освободить пользователей моего виджета от столь избыточного кода, в данном случае ещё и не имеющего смысл. Пришлось много прочитать про создание кастомных компонентов-контейнеров. Но и там не было нужной информации. И только исследование исходников GridLayout дало ответ на этот вопрос. Оказывается, достаточно переопределить generateLayoutParams и возвратить ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT). И это всё — теперь все дочерние виджеты можно указывать без атрибутов высоты и ширины.
Напоследок расскажу об одной интересной фиче, на которую я наткнулся. Можно добиться сокращённого использования ScreenPager и без LayoutInflater.from(this).setFactory. Давайте рассуждать логически. Андроид ищет сокращённые имена в пакете android.view. Собственно, ничего не мешает нам в нашей папке src создать папку android наравне с папкой com. А в ней уже папку view и там разместить файл ScreenPager.java, не забыв поменять название пакета внутри самого файла. И это отлично компилируется и запускается! Попробуйте. Однако помещать свои классы в чужие пакеты — это не очень хороший подход. Скорее просто интересный момент.
Автор: kciray