Pull to refresh

Sliding экранов внутри приложения

Reading time 6 min
Views 54K


Здравствуйте!

Современные мобильные устройства в первую очередь ориентированы на управление пальцами, в частности жестами и прикосновениями. Эта особенность добавляет определенную специфику в организацию пользовательских интерфейсов. На главную роль выходят интуитивно понятные решения, требующие от пользователя наименьших усилий и при этом ненавязчивые, сохраняющие максимум полезного пространства, т.е. не загромождающие пользовательский интерфейс.

Примером таких решений является переключение между рабочими экранами в Android, где для перемещения от одного экрана к другому достаточно выполнить жест вправо или влево. О возможной реализации подобного решения и пойдет речь в данном посте.

Вместо вступления


Для того чтобы проиллюстрировать, то о чем будет идти речь далее, и тем самым добавить наглядности, я записал небольшое видео работы приложения, которое я сегодня буду делать:



В показанном выше приложении имеются четыре несвязанные View, перемещение между которыми осуществляется жестами вправо/влево по экрану. Содержание View могло быть произвольное, но для простоты восприятия я сделал их разноцветными и пронумерованными.

Подобная функциональность не является базовой, т.е. не имеет полностью готового встроенного решения. А также, как ни странно, не является сильно распространенной, и, наверно, из-за этого не освещена в интернете в достаточной степени.

Разработка


Для реализации приведенного приложения необходимо выполнить следующие шаги:
  1. Найти или реализовать контейнер для удобного хранения и перемещения между View
  2. Перехватить пользовательские жесты и на основании полученной информации сделать вывод в какую сторону «двигать» view
  3. Добавить анимацию для визуализации перемещений между экранами
К счастью, контейнер, отвечающий нашим требованиям, уже присутствует в арсенале Android, называется он ViewFlipper. Создадим пустой проект, и добавим в основной layout элемент ViewFlipper:
<?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.
Tags:
Hubs:
+34
Comments 33
Comments Comments 33

Articles