tutorial swipeable_ru - oxygine/oxygine-framework GitHub Wiki

Давайте разработаем компонент Swipeable

Начало

Суть его работы проста: двигаем по экрану пальцем или мышкой слева направо или справа налево и листаем страницы.

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

За основу для разработки берем пример из папки oxygine-framework/examples/HelloWorld. Открываем проект в вашей любимой IDE. Из файла example.cpp удаляем все лишнее, оставляя только основные функции и загрузку ресурсов. Для простоты и наглядности весь код будет редактироваться и добавляться только в нем.

#include "oxygine-framework.h"
using namespace oxygine;


//it is our resources
//in real project you would have more than one Resources declarations.
//It is important on mobile devices with limited memory and you would load/unload them
Resources gameResources;

void example_preinit() {}

//called from main.cpp
void example_init()
{
    //load xml file with resources definition
    gameResources.loadXML("res.xml");
}

//called each frame from main.cpp
void example_update()
{
}

//called each frame from main.cpp
void example_destroy()
{
    //free previously loaded resources
    gameResources.free();
}

https://github.com/oxygine/oxygine-tutorial-swipeable/blob/3ae2aa5815311e6846a2514e282a079b42d27369/src/example.cpp

Можно скомпилировать, запустить и увидим пустой черный экран.

Откроем файл с ресурсами и добавим туда наши картинки, не забыв скопировать их в папку data/images.

<?xml version="1.0"?>
<resources>
    <set path = "images" />
    <atlas>
        <image file="img1.png" />
        <image file="img2.png" />
        <image file="img3.png" />
        <image file="img4.png" />       
    </atlas>    
</resources>

https://github.com/oxygine/oxygine-tutorial-swipeable/blob/fc54d35cf41133786c623aaa95cb5279dc1c7a28/data/res.xml

А теперь приступим. Первым делом выведем на экране первую картинку img1.png. Для этого создаем Actor типа Sprite и прицепляем ее к самому верхнему элементу нашей сцены Stage.

Actor типа Stage существует только в одном экземпляре и может быть получен в с помощью функции getStage()

Кстати, сам Stage создается и настраивается в main.cpp

void example_init()
{
    //load xml file with resources definition
    gameResources.loadXML("res.xml");

    spSprite sprite = new Sprite;
    sprite->setResAnim(gameResources.getResAnim("img1"));
    sprite->attachTo(getStage());
}

Компилируем и запускаем.

Переходим к обработке касаний. Добавим пустые заглушки для обработки событий прикосновения и отпускания пальца:

sprite->addEventListener(TouchEvent::TOUCH_DOWN, [=](Event*) {
    logs::messageln("touch down");
});

sprite->addEventListener(TouchEvent::TOUCH_UP, [=](Event*) {
    logs::messageln("touch up");
});

Если сейчас запустить приложение, то можно понаблюдать за выводом в консоль/output окно:

Для теста можно попробовать такое:

sprite->addEventListener(TouchEvent::TOUCH_DOWN, [=](Event*) {
    logs::messageln("touch down");
    sprite->addTween(Actor::TweenX(500), 500);
});

sprite->addEventListener(TouchEvent::TOUCH_UP, [=](Event*) {
    logs::messageln("touch up");
    sprite->addTween(Actor::TweenX(0), 1000);
});

https://github.com/oxygine/oxygine-tutorial-swipeable/commit/02b49bf2e44b2677440023588ec4f68ec70afa76

То есть при нажатии (событие TOUCH_DOWN) наш спрайт уезжает к краю экрана в точку 500 по оси X за 500 миллисекунд, а при отпускании (событие TOUCH_UP) возвращается обратно в точку 0 за 1000 миллисекунд. Перемещение спрайта производится с помощью встроенной в oxygine функциональности Tweening.

В отличии от события TOUCH_DOWN, которое срабатывает только при попадании по спрайту, событие TOUCH_UP может сработать, даже если касание было сделано мимо спрайта (но только в том случае если он был зажат перед этим).

Подробнее изучить обработку событий можно тут.

Попытаемся теперь отследить событие Swipe. Нам понадобится дополнительно обработчик движения мыши TouchEvent::MOVE. Смотрим что получилось:

bool pressed = false;
Vector2 downPos(0,0);

void example_init()
{
    ... //skipped some code

    sprite->addEventListener(TouchEvent::TOUCH_DOWN, [=](Event* ev) {
        logs::messageln("touch down");

        TouchEvent *touch = (TouchEvent*)ev;
        downPos = touch->localPosition;
        pressed = true;
    });

    sprite->addEventListener(TouchEvent::TOUCH_UP, [=](Event*) {
        logs::messageln("touch up");
        //sprite->addTween(Actor::TweenX(0), 1000);
        pressed = false;
    });


    sprite->addEventListener(TouchEvent::MOVE, [=](Event* ev) {
        if (!pressed)
            return;

        TouchEvent *touch = (TouchEvent*)ev;
        Vector2 dir = touch->localPosition - downPos;
        if (dir.x < -50)
        {
            pressed = false;
            logs::messageln("swipe left");
        }
        if (dir.x > 50)
        {
            pressed = false;
            logs::messageln("swipe right");
        }
    });
}

Изменения:

  1. Я закомментировал старый код с addTween.
  2. Добавил 2 переменные.
  3. В TOUCH_DOWN обработчике запоминаем, что палец был нажат и его координаты.
  4. В MOVE обработчике смотрим на сколько сдвинулся палец относительно сохраненной координаты, если по X преодолен порог в 50 пикселей, то засчитываем как Swipe и выводим сообщение в лог.

Идем дальше, добавляем визуальное движение картинки по экрану:

sprite->addEventListener(TouchEvent::MOVE, [=](Event* ev) {
        if (!pressed)
            return;


        TouchEvent *touch = (TouchEvent*)ev;
        Vector2 dir = touch->localPosition - downPos;
        if (dir.x < -50)
        {
            pressed = false;
            logs::messageln("swipe left");
            sprite->addTween(Actor::TweenX(sprite->getX() - sprite->getWidth()), 300);
        }
        if (dir.x > 50)
        {
            pressed = false;
            logs::messageln("swipe right");
            sprite->addTween(Actor::TweenX(sprite->getX() + sprite->getWidth()), 300);
        }
});

https://github.com/oxygine/oxygine-tutorial-swipeable/commit/a9c47193cf7922c6616606248792d64d3bc1a781

Запускаем, делаем первый Swipe и натыкаемся на проблему: повторные swipe не происходят. Почему? Потому что наш sprite уехал за границы экрана и мы больше не можем по нему попасть мышкой. Как это решить? Если подумать, то нам не важно какого он размера и куда он уезжает, нас интересует только само событие Swipe на экране. Потому перевешиваем наши EventListeners со sprite на Stage:

getStage()->addEventListener(TouchEvent::TOUCH_DOWN, ...
getStage()->addEventListener(TouchEvent::TOUCH_UP, ...
getStage()->addEventListener(TouchEvent::MOVE, …

https://github.com/oxygine/oxygine-tutorial-swipeable/commit/83af0f97b41cd86a5d9e1b8c3568ec99e2979239

Стало лучше. Одновременно с этим мы решили проблему, когда “touch->localPosition” зависит от координат sprite и меняется во время его движения, что приводит к полному хаосу, если делать Swipe очень быстро. Теперь же touch->localPosition привязан к системе координат Stage, которая статична в отличии от sprite.

И вот у нас готова базовая функциональность. Дальше дело техники, создавать последовательно другие картинки по мере Swipe и двигать их.

Дорабатываем наш блок кода, отвечающий за swipe left (swipe right не привожу, чтобы не загромождать, он принципиально ничем не отличается):

int current = 0;
spSprite sprite;
const char* images[] = {"img1", "img2", "img3", "img4"};


void example_init()
{
    //load xml file with resources definition
    gameResources.loadXML("res.xml");


    sprite = new Sprite;

        ... //skipped some code

        if (dir.x < -50)
        {
            pressed = false;
            logs::messageln("swipe left");


            sprite->addTween(Actor::TweenX(-sprite->getWidth()), 300);
            current = (current + 1) % 4;


            spSprite nextSprite = new Sprite;
            nextSprite->setResAnim(gameResources.getResAnim(images[current]));
            nextSprite->setX(sprite->getWidth());
            nextSprite->addTween(Actor::TweenX(0), 300);
            nextSprite->attachTo(getStage());
            sprite = nextSprite;
        }

https://github.com/oxygine/oxygine-tutorial-swipeable/commit/f313068fe984ee4d8c801692da3dd3bbcfa80fbd

Изменения:

  1. Обратите внимание на новые переменные.
  2. Объект sprite теперь хранится глобально, чтоб мы могли его переназначить.
  3. Добавили массив из списка картинок и индекс текущей.

Данный код не лишен недостатков, самое очевидное - при быстрых Swipeспрайты будут создаваться преждевременно, но пока мы не будем обращать на это внимание.

У нас есть утечка!!! sprite = nextSprite; Мы забыли про старый спрайт, но он никуда не делся, он продолжает висеть в Stage, даже если его не видно, полистайте пару раз и загляните в Tree Inspector. Он отображает полную иерархию всех созданных объектов в Stage:

На экране сейчас зеленая картинка, а первые 3 где-то висят в позиции (-960,0) за пределами видимости.

Избавляемся от них. Строку в swipe left:

sprite->addTween(Actor::TweenX(-sprite->getWidth()), 300);

Дополняем до:

spTween tween = sprite->addTween(Actor::TweenX(-sprite->getWidth()), 300);
tween->detachWhenDone();

То есть по завершению анимации перемещения sprite будет автоматически detached от родителя. А так как все (почти все) объекты в oxygine унаследованы от счетчика ссылок и хранятся как smart pointers, то потерянный sprite автоматически будет deleted из памяти.

https://github.com/oxygine/oxygine-tutorial-swipeable/commit/d488d7f53f16016e538800ed82b9f3919dde6e08

Запускаем пример, делаем пару раз swipe left и убеждаемся что теперь в Stage только 1 спрайт, тот который мы сейчас видим:

Выводы

На данном этапе мы добились базовой функциональности Swipe и можем свободно листать по экрану пальцем картинки. Чего тут не хватает?

  1. Надо оформить этот код как отдельный компонент, с возможностью его последующего переиспользования.
  2. Нет корректной работы при быстрых Swipe.
  3. Нет поддержки произвольных размеров.
  4. Нужна возможность рисовать элементы в окошке, отсекая все что за пределами. (пригодится ClipRectActor).
⚠️ **GitHub.com Fallback** ⚠️