
Здравствуйте!
Современные мобильные устройства в первую очередь ориентированы на управление пальцами, в частности жестами и прикосновениями. Эта особенность добавляет определенную специфику в организацию пользовательских интерфейсов. На главную роль выходят интуитивно понятные решения, требующие от пользователя наименьших усилий и при этом ненавязчивые, сохраняющие максимум полезного пространства, т.е. не загромождающие пользовательский интерфейс.
Примером таких решений является переключение между рабочими экранами в Android, где для перемещения от одного экрана к другому достаточно выполнить жест вправо или влево. О возможной реализации подобного решения и пойдет речь в данном посте.
Вместо вступления
Для того чтобы проиллюстрировать, то о чем будет идти речь далее, и тем самым добавить наглядности, я записал небольшое видео работы приложения, которое я сегодня буду делать:
В показанном выше приложении имеются четыре несвязанные View, перемещение между которыми осуществляется жестами вправо/влево по экрану. Содержание View могло быть произвольное, но для простоты восприятия я сделал их разноцветными и пронумерованными.
Подобная функциональность не является базовой, т.е. не имеет полностью готового встроенного решения. А также, как ни странно, не является сильно распространенной, и, наверно, из-за этого не освещена в интернете в достаточной степени.
Разработка
Для реализации приведенного приложения необходимо выполнить следующие шаги:
- Найти или реализовать контейнер для удобного хранения и перемещения между View
- Перехватить пользовательские жесты и на основании полученной информации сделать вывод в какую сторону «двигать» view
- Добавить анимацию для визуализации перемещений между экранами
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main_layout"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ViewFlipper
android:id="@+id/flipper"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
</LinearLayout>
Теперь создадим четыре view между которыми будем перемещаться, в моем примере они имеют вид:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:background="@android:color/white"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:text="1"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_centerInParent="true"
android:textSize="140px"
android:textStyle="bold"/>
</RelativeLayout>
Далее необходимо связать имеющиеся view c ViewFlipper. Для этого в методе OnCreate базового Activity добавим следующий код:
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Устанавливаем listener касаний, для последующего перехвата жестов
LinearLayout mainLayout = (LinearLayout) findViewById(R.id.main_layout);
mainLayout.setOnTouchListener(this);
// Получаем объект ViewFlipper
flipper = (ViewFlipper) findViewById(R.id.flipper);
// Создаем View и добавляем их в уже готовый flipper
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
int layouts[] = new int[]{ R.layout.first, R.layout.second, R.layout.third, R.layout.fourth };
for (int layout : layouts)
flipper.addView(inflater.inflate(layout, null));
}
Теперь мы можем перемещаться между view вызовами методов showNext() и showPrevious() объекта flipper.
Жесты пользователя возбуждают события OnTouch, для обработки этих событий необходимо реализовать метод:
boolean onTouch(View view, MotionEvent event)
В этом методе, в объекте класса MotionEvent, присутствует вся необходимая информация и выполненном жесте.
Т.к. нам нужно понять куда был выполнен жест вправо или влево, то мы поступим очень просто: сохраним координату по оси X, при нажатии на экран, и сравним с координатой после того как экран будет отпущен. Если изначальное значение больше, то это движение влево, иначе вправо. Код выглядит достаточно просто:
public boolean onTouch(View view, MotionEvent event)
{
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN: // Пользователь нажал на экран, т.е. начало движения
// fromPosition - координата по оси X начала выполнения операции
fromPosition = event.getX();
break;
case MotionEvent.ACTION_UP: // Пользователь отпустил экран, т.е. окончание движения
float toPosition = event.getX();
if (fromPosition > toPosition)
flipper.showNext();
else if (fromPosition < toPosition)
flipper.showPrevious();
default:
break;
}
return true;
}
Осталось добавить анимацию. ViewFlipper как и все наследники ViewAnimator поддерживает методы: setInAnimation(...) и setOutAnimation(...), через которые можно задать анимации (Animation) вхождения view в экран и пропадания view с экрана. Но т.к. наша анимация будет отличаться в зависимости от жестов, то придется ее указывать каждый раз заново, исходя из текущей операции. Т.е. метод onTouch нужно модифицировать следующим образом:
...
if (fromPosition > toPosition)
{
flipper.setInAnimation(AnimationUtils.loadAnimation(this,R.anim.go_next_in));
flipper.setOutAnimation(AnimationUtils.loadAnimation(this,R.anim.go_next_out));
flipper.showNext();
}
else if (fromPosition < toPosition)
{
flipper.setInAnimation(AnimationUtils.loadAnimation(this,R.anim.go_prev_in));
flipper.setOutAnimation(AnimationUtils.loadAnimation(this,R.anim.go_prev_out));
flipper.showPrevious();
}
...
Где R.anim.* ресурсы с анимацией появления и исчезания view. Не буду приводить все четыре варианта приведу только первый (все остальные можно будет посмотреть в примере проекта, который я приложу к посту):
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:fromXDelta="100%p"
android:toXDelta="0"
android:duration="400"/>
<alpha
android:fromAlpha="1.0"
android:toAlpha="1.0"
android:duration="400" />
</set>
Все готово! Проект с примером можете скачать отсюда.
UPD. Вариант ViewFlipper'а без отпускания пальца (изменение метода onTouch):
...
// Вместо ACTION_UP
case MotionEvent.ACTION_MOVE:
float toPosition = event.getX();
// MOVE_LENGTH - расстояние по оси X, после которого можно переходить на след. экран
// В моем тестовом примере MOVE_LENGTH = 150
if ((fromPosition - MOVE_LENGTH) > toPosition)
{
fromPosition = toPosition;
flipper.setInAnimation(AnimationUtils.loadAnimation(this,R.anim.go_next_in));
flipper.setOutAnimation(AnimationUtils.loadAnimation(this,R.anim.go_next_out));
flipper.showNext();
}
else if ((fromPosition + MOVE_LENGTH) < toPosition)
{
fromPosition = toPosition;
flipper.setInAnimation(AnimationUtils.loadAnimation(this,R.anim.go_prev_in));
flipper.setOutAnimation(AnimationUtils.loadAnimation(this,R.anim.go_prev_out));
flipper.showPrevious();
}
...
UPD 2. Если вам больше нравится слайдинг с предпросмотром следующей страницы, то вы можете присмотреться к решениям без ViewFlipper, на базе ViewGroup и scrollTo, пример можете найти, например, здесь.
Заключение
Основное достоинство sliding'а, при правильном его использовании: разгрузка пользовательского интерфейса от избыточных контролов. Как видно, реализация получилась простой, а применение подобного функционала поистине широкое.
Но ложка дегтя все же присутствует. Во-первых, если взглянуть на мой пример в первый раз, то практически невозможно догадаться, что там присутствует sliding (это пример и здесь я такой задачи не ставил), а следовательно применение sliding'а требует наличие других UI-решений поясняющих наличие этого самого sliding'а, например, на тех же home screen'ах Android есть «ползунок-индикатор» показывающий на каком экране находится в данный момент пользователь. Во-вторых, при наличии на view контролов, имеющих собственную реакцию на нажатия, прокрутки, и т.п., вызов метода onTouch внутри ViewFlipper'а будет заблокирован его более ранним перехватом внутри дочернего view, поэтому придется дополнительно побеспокоиться о пробрасывании этого события «наверх» к ViewFlipper.