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



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

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

    Примером таких решений является переключение между рабочими экранами в 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.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 33

      0
      Классный подход для каких-нибудь читалок. Хотя, если читалка, то всё-таки лучше было бы реализовать полноценный page turn effect, тогда и ViewFlipper особо не нужен, потому что всю логику можно поместить в {currentView, prevView, nextView}, анимировать между ними, кэшируя более «далекие» элементы с неким, экономя память на хранении в контейнере.
        0
        Извиняюсь. «с неким оффсетом».
        Когда уже сделают редактирование комментариев?
          0
          А никогда, что бы сразу думали что пишут и не исправляли где не надо!
            0
            Комментарий не воробей :)
          0
          вскольз посмотрел статью…
          если правильно понял, что получиться в этиге, то это слайдинг для бедных какой-то.
          под нормальным слайдинго я подразумеваю такое как в TweetDeck, GO Sms pro, лаучнерах между экранами.
            0
            простите за опечатки, клавиатура неудобно лежала, а проверить текст перед отправлением забыл.
              +1
              Здесь описан подход к тому как это можно начинать делать. Никто не мешает накрутить, то, что вам нужно, а это уже зависит от задач приложения.
                0
                В моем случае и подход отличался.

                Просто когда понадобилось сделать слайдинг, везде описан вариант похожий на ваш, а как сделать такое как в TweetDeck — так и не нашел, сначала изобретал велосипед, потом посмотрел в исходниках анродида как они сделали в своем ланчере.
                  0
                  И как они сделали? Расскажите вкратце.
                    0
                    Простите, увидел ниже
              +2
              По-моему, когда слайдинг начинается после отпускания, то создается впечатление, что телефон глючит. Горизонтальный слайдинг должен быть таким же, как и вертикальный, т.е. контент должен прокручиваться непосредственно под пальцем, то бишь в реальном времени.

              В качестве примера такого слайдинга могу привести Pulse News и YouTube.
                0
                Это не сложно добавить, нужно заменить событие ACTION_UP на ACTION_MOVE (т.е. на перемещение, а не отпускание) и немного модифицировать код обработки, чтобы он срабатывал не сразу после начала перемещения, а после какого-то промежутка.
                  +3
                  а можно
                  — воспользоваться ViewGroup и стандартным скроллером (методом scrollTo);
                  — правильно обрабатывать отпускание (доскролить до конца в какую-то из сторон);
                  — на onLayout, упорядочить все сабвью и делать их тогоже размера, что и наш ViewGroup;
                  — важно еще правильно редиректить события, что бы нажатия на дочерних элементах работали.

                  файл из исхдников гуглового ланчера в котором подобным образом реализован слайдинг между рабочими столами:
                  grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android-apps/2.1_r2/com/android/launcher2/Workspace.java
                    0
                    Спасибо за ссылку.
                      +1
                      Добавил в пост ссылку на готовую реализацию ViewGroup + scrollTo, без ViewFlipper.
                        0
                        о. здорово.

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

                        уверен, что это сэкономит кому-то время.
                  0
                  Есть табы и заходя в один из табов необходимо переходить между экранами туда-сюда, при это сами табы должны оставаться на своих местах? При этом не хочется смешивать логику и представления в одном вью( а как иначе если использовать ViewFlipper?). Так вот, ViewFlipper поможет все таки? На сколько я понимаю, он листает только наследники View, а у меня есть Activity, логика в них и лэйауты отдельные для каждого экрана.
                  Что можете посоветовать?
                  Использование ActivityGroup приводит к StackOverFlow, точнее вложенность вьюшек(хотя их вложенность явно меньше 10)
                    0
                    А вы не пробовали ненужные активити завершать? Делать им finish()? Или они все вам вместе нужны?
                  0
                  Спасибо за статью. Периодически возвращался к мысли что надо как-то сделать эту хренотеть — в жизни пригодится — но лень брала свое. Лень была права!
                    0
                    На самом деле очень простое но не очень удачное решение, для именно такого эффекта лучше использовать подход аналогичный рабочим столам, потому что пользователю постоянно работает с ними и привык получать отзыв в виде сдвига view до того как он отпускает палец.
                      0
                      Я написал чуть выше, не сложно изменить логику на слайдинг без отпускания пальца.
                        0
                        С viewFlipperom так не получится.
                          0
                          Тогда я видимо вас неправильно понял, вы имели в виду слайдинг между экранами без отпускания пальца или что-то другое?
                      0
                      Я имел ввиду эффект смены рабочих столов, к которому так привыкли пользователи. Когда изображение смещается пропорционально изменению координат в ACTION_MOVE(на каждом событии), а окончательный переход на рабочий стол происходит после ACTION_UP в зависимости от того было ли итоговое смещение положительным или отрицательным. К сожалению описанным выше способом не получится этого сделать.
                        0
                        Соглашусь, если нужно видеть содержимое следующего экрана до окончательного перемещения на него, то ViewFlipper не подойдет.
                          0
                          А как это реализуется?
                            0
                            Вот здесь можете взять готовый пример на базе ViewGroup и scrollTo.
                          0
                          Я вот не могу понять, а почему бы вместо обработки вручную не использовать GetureDetector.onFling?

                          Еще описанную функциональность можно получить просто настроив Gallery (только нужно перенаправлять ей жесты).
                            0
                            public boolean onTouch(View v, MotionEvent event) {
                            ViewFlipper flipper = (ViewFlipper) findViewById(R.id.flipper);
                            // TODO Auto-generated method stub
                            switch (event.getAction()) {
                            case MotionEvent.ACTION_DOWN:
                            float 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;

                            ругаетсо на то что переменная fromPosition — необьявлена… где косяк?
                              0
                              Переменная должна быть объявлена вне метода иначе она будет перезаписана, т.е. должна быть объявлена внутри класса.
                              В статье есть линк на исходники, посмотрите, там все понятно.
                                0
                                сделал так

                                public class Main extends Activity implements OnTouchListener {
                                float fromPosition;
                                float toPosition;


                                скомпилился но не работает
                                  0
                                  А toPosition зачем вы вынесли? Вообщем возьмите проект по ссылке, попробуйте разобраться в нем (для этого и была статья), а потом попробуйте запустить его.
                                  Причин почему может не работать может быть много, догадаться что именно произошло у вас по трем строчкам невозможно.

                            Only users with full accounts can post comments. Log in, please.