понедельник, 15 октября 2012 г.

7. Двигаем картинками (часть 2)

Используя механизмы анимации, реализованные в предыдущей статье, создадим небольшой пользовательский интерфейс, для управления настройками типичного Android-приложения. Для начала, нам понадобятся кнопки. Кнопки должны реагировать на нажатие, выполняя простую анимацию:

Button.h:

#ifndef _BUTTON_H_
#define _BUTTON_H_

#include "AnimatedSprite.h"
#include "AbstractSpriteOwner.h"

enum EButtonMessage {
    ebmDown             = 0x0100,
    ebmUp               = 0x0101,
    ebmOutUp            = 0x0111,
    ebmPressed          = 0x0102
};

class Button: public AnimatedSprite {
    protected:
        AnimateMessage* msgDown;
        AnimateMessage* msgUp;
        int message;
        AbstractSpriteOwner* receiver;
        void configure();
    public:
        Button(ISpriteOwner* scene, const char* res, int x, int y, 
               int zOrder = 0, int loc = elNothing);
        Button(ISpriteOwner* scene, int x, int y, int zOrder = 0);
        virtual bool isValidMessage(int msg);
        virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL);
        virtual void doMessage(int msg, void* data = NULL, uint64 timestamp = 0);
        virtual bool isPausable() const {return false;}
        void addReceiver(int m, AbstractSpriteOwner* r);
};

#endif    // _BUTTON_H_

Button.cpp:

#include "Button.h"
#include "Desktop.h"
#include "MoveAction.h"
#include "SendMessageAction.h"
#include "SoundAction.h"

Button::Button(ISpriteOwner* scene, const char* res, int x, int y, 
             int zOrder, int loc): AnimatedSprite(scene, res, x, y, zOrder, loc),
                                   receiver(NULL) {
    Button::configure();
}

Button::Button(ISpriteOwner* scene, int x, int y, int zOrder): 
               AnimatedSprite(scene, x, y, zOrder), 
               receiver(NULL) {
    Button::configure();
}

void Button::configure() {
    msgDown = new AnimateMessage();
    msgDown->addAction(new MoveAction(this, 0, 50, 10, 10));
    msgDown->addAction(new SoundAction(this, 50, "menubutton"));
    addMessageRule(ebmDown, msgDown);
    msgUp = new AnimateMessage();
    msgUp->addAction(new MoveAction(this, 100, 150, 0, 0));
    addMessageRule(ebmOutUp, msgUp);
    msgUp = new AnimateMessage();
    msgUp->addAction(new MoveAction(this, 100, 150, 0, 0));
    msgUp->addAction(new SendMessageAction(this, 100, ebmPressed));
    msgUp->addAction(new SendMessageAction(this, 110, emtInit));
    addMessageRule(ebmUp, msgUp);
}

bool Button::isValidMessage(int msg) {
    switch (msg) {
        case emtTouchDown:
        case   emtTouchUp:
        case      ebmDown:
        case        ebmUp:
        case     ebmOutUp:
        case   ebmPressed: return true;
        default: return AnimatedSprite::isValidMessage(msg);
    }
}

void Button::doMessage(int msg, void* data, uint64 timestamp) {
    if (msg == ebmPressed) {
        if (receiver != NULL) {
            receiver->sendMessage(message, 0, (IObject*)this);
        }
        return;
    }
    AnimatedSprite::doMessage(msg, data, timestamp);
}

bool Button::sendMessage(int msg, uint64 timestamp, void* data) {
    if ((msg & emtTouchEvent) != 0) {
        switch (msg & emtTouchMask) {
            case emtTouchDown:
                sendMessage(ebmDown, desktop.getCurrentTimestamp());
                break;
            case emtTouchUp:
                sendMessage(ebmUp, desktop.getCurrentTimestamp());
                break;
            case emtTouchOutUp:
                sendMessage(ebmOutUp, desktop.getCurrentTimestamp());
                break;
        }
        return true;
    }
    return AnimatedSprite::sendMessage(msg, timestamp, data);
}

void Button::addReceiver(int m, AbstractSpriteOwner* r) {
    message  = m;
    receiver = r;
}

В методе sendMessage мы обрабатываем коды событий Touchpad, формируя внутренние события, с которыми работает кнопка. В configure с этими событиями связывается анимация, при нажатии, кнопка перемещается на 10 единиц вниз и вправо, при отпускании, возвращается на место. В случае, если при отпускании точка касания не ушла с кнопки, формируется событие ebmPressed, с которым мы можем связать произвольный обработчик. Реализацию SoundAction мы рассмотрим чуть позже.
Помимо обычных кнопок, нам понадобятся кнопки-переключатели, с изменяемой картинкой. Поскольку эта кнопка также должна анимироваться при нажатии, унаследуем ее от Button:

SwitchButton.h:

#ifndef _SWITCHBUTTON_H_
#define _SWITCHBUTTON_H_

#include "Button.h"

class SwitchButton: public Button {
    protected:
        void configure();
    public:
        SwitchButton(ISpriteOwner* scene, int x, int y, int zOrder = 0);
        virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL);
};

#endif    // _SWITCHBUTTON_H_

SwitchButton.cpp:

#include "SwitchButton.h"
#include "Desktop.h"
#include "MoveAction.h"
#include "SendMessageAction.h"
#include "SoundAction.h"

SwitchButton::SwitchButton(ISpriteOwner* scene, int x, int y, int zOrder): 
                           Button(scene, x, y, zOrder) {
    SwitchButton::configure();
}

void SwitchButton::configure() {
    msgUp->addAction(new SendMessageAction(this, 50, emtSwitch));
}

bool SwitchButton::sendMessage(int msg, uint64 timestamp, void* data) {
    if (msg == emtSwitch) {
        doMessage(msg, 0, timestamp);
        if (receiver != NULL) {
            receiver->sendMessage(message, 0, (IObject*)this);
        }
        return true;
    }
    return Button::sendMessage(msg, timestamp, data);
}


Мы просто добавляем в анимацию отпускания кнопки формирование события emtSwitch, переключающее изображение для любого AnimatedSprite.
В анимации Button мы использовали ранее неописанный нами SoundAction, который будет вызывать проигрывание звука нажимающейся кнопки. Рассмотрим его реализацию:

SoundAction.h:

#ifndef _SOUNDACTION_H_
#define _SOUNDACTION_H_

#include <string>
#include "IwSound.h"

#include "AnimateAction.h"
#include "Locale.h"

using namespace std;

class SoundAction: public AnimateAction {
    private:
        string res;
        int loc;
        bool checkSound();
    protected:
        virtual void doAction(int timeDelta);
    public:
        SoundAction(AbstractScreenObject* sprite, uint64 timeDelta, const char* r,
                    int loc = elSound);
};

#endif    // _SOUNDACTION_H_

SoundAction.cpp:

#include "SoundAction.h"
#include "Desktop.h"

SoundAction::SoundAction(AbstractScreenObject* sprite, uint64 timeDelta,
         const char* r, int loc): AnimateAction(sprite, timeDelta, timeDelta)
         , res(r), loc(loc) {
}

void SoundAction::doAction(int timeDelta) {
    CIwResGroup* resGroup;
    const char* groupName = Locale::getGroupName(loc);
    if (checkSound() &&(groupName != NULL)) {
        resGroup = IwGetResManager()->GetGroupNamed(groupName);
        CIwSoundSpec* SoundSpec = (CIwSoundSpec*)resGroup->GetResNamed(res.c_str(),
                                  IW_SOUND_RESTYPE_SPEC);
        CIwSoundInst* SoundInstance = SoundSpec->Play();
    }
}

bool SoundAction::checkSound() {
    IObject* o = (IObject*)desktop.getName("soundon");
    if (o != NULL) {
        return (o->getState() != 0);
    }
    return false;
}


Этот пример показывает, как легко мы можем расширять список действий, выполняемых при анимации. 
В методе checkSound проверяется состояние объекта с именем "soundon", для определения того, должен ли проигрываться звуковой эффект. Этот объект представляет собой SwitchButton с состояниями 0 и 1. который мы реализуем в нашем интерфейсе. Для того, чтобы искать объекты пользовательского интерфейса по имени, потребуется внести в код некоторые дополнения:

Desktop.h:

#ifndef _DESKTOP_H_
#define _DESKTOP_H_

#include <set>
#include <map>

. . .
class Desktop {
    private:
                  . . .
        map<string, void*> names;
    public:
        Desktop(): touches(), names(), currentScene(NULL) {}
                   . . .
        void setName(string name, void* o);
        void* getName(string name);

    typedef map<string, void*>::iterator NIter;
    typedef pair<string, void*> NPair;
};
       
extern Desktop desktop;

#endif    // _DESKTOP_H_

Desktop.cpp:

#include "Desktop.h"
#include "Iw2D.h"

#include "TouchPad.h"

Desktop desktop;

. . .
void Desktop::release() {
          . . .
    names.clear();
}

void Desktop::setName(string name, void* o) {
    NIter p = names.find(name);
    if (p != names.end()) {
        names.erase(p);
    }
    names.insert(NPair(name, o));
}

void* Desktop::getName(string name) {
    NIter p = names.find(name);
    if (p == names.end()) {
        return NULL;
    }
    return p->second;
}

...

AbstractScreenObject.h:

#ifndef _ABSTRACTSCREENOBJECT_H_
#define _ABSTRACTSCREENOBJECT_H_

#include <string>
#include "Iw2D.h"

#include "IScreenObject.h"

using namespace std;

class AbstractScreenObject: public IScreenObject {
    public:

                . . .
        void setName(string name);
        static IObject* getName(string name);
};

#endif    // _ABSTRACTSCREENOBJECT_H_


AbstractScreenObject.cpp:

#include "AbstractScreenObject.h"
#include "Desktop.h"

...
void AbstractScreenObject::setName(string name) {
    desktop.setName(name, (IObject*)this);
}

IObject* AbstractScreenObject::getName(string name) {
    return (IObject*)desktop.getName(name);
}


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




Теперь все готово для разработки пользовательского интерфейса. Переносим код создания сцены из Main.cpp в новый класс Intro:

Main.cpp:

#include "Main.h"
...
#include "Intro.h"
 
...
int main() {
    init();    {

        Intro  intro;
        desktop.setScene(&intro);
        ...
    }
    release();
    return 0;
}
Intro.h:

#ifndef _INTRO_H_
#define _INTRO_H_

#include "Scene.h"
#include "CompositeSprite.h"

enum EIntroMessage {
        eimPlay              = 0x100,
        eimSettings          = 0x101,
        eimBack              = 0x102,
        eimCheckMusic        = 0x103
};

enum EIntroStatus {
        eisMain              = 0,
        eisSettings          = 1
};

class Intro: public Scene {
    private:
        Sprite* background;
        CompositeSprite* title;
        CompositeSprite* menu;
        CompositeSprite* settings;
        int state;
        void checkMusic();
    protected:
        virtual bool doKeyMessage(int msg, s3eKey key);
        virtual int  getState() {return state;}
        void setState(int s) {state = s;}
    public:
        Intro();
        virtual bool init();
        virtual bool sendMessage(int msg, uint64 timestamp = 0, void* data = NULL);
};

extern Intro* introScene;

#endif    // _INTRO_H_

Intro.cpp:

#include "Intro.h"
#include "Background.h"
#include "IntroTitle.h"
#include "IntroMenu.h"
#include "IntroSound.h"
#include "Desktop.h"

Intro* introScene = NULL;

Intro::Intro(): state(eisMain) {
    introScene = this;
}

bool Intro::init() {
    if (!Scene::init()) return false;
    regKey(s3eKeyBack);
    regKey(s3eKeyAbsBSK);
#if defined IW_DEBUG
    regKey(s3eKeyLSK);
#endif
    background = new Background(this, "background.png", 1);
    title = new IntroTitle(this, 2);
    menu = new IntroMenu(this, 3);
    settings = new IntroSound(this, 4);
    settings->doMessage(emtHide);
    checkMusic();
    return true;
}

bool Intro::doKeyMessage(int msg, s3eKey key) {
     if (msg == emtKeyPressed) {
        switch (state) {
            case eisSettings:
                sendMessage(eimBack);
                return true;
        }
    }
    return false;
}

bool Intro::sendMessage(int msg, uint64 timestamp, void* data) {
    switch (msg) {
        case eimPlay:
            // TODO:
            return true;
        case eimSettings:
            background->setAlpha(IW_2D_ALPHA_HALF);
            title->doMessage(emtHide);
            menu->doMessage(emtHide);
            settings->doMessage(emtShow);
            setState(eisSettings);
            return true;
        case eimBack:
            background->setAlpha(IW_2D_ALPHA_NONE);
            title->doMessage(emtShow);
            menu->doMessage(emtShow);
            settings->doMessage(emtHide);
            setState(eisMain);
            return true;
        case emtInit:
        case eimCheckMusic:
            checkMusic();
            return true;
    }
    return false;
}

void Intro::checkMusic() {
    bool f = false;
    IObject* o = (IObject*)desktop.getName("musicon");
    if (o == NULL) {
        desktop.stopMusic();
        return;
    }
    f = (o->getState() != 0);
    if (f) {
        desktop.startMusic("music.mp3");
    } else {
        desktop.stopMusic();
    }
}


Класс Intro представляет единственную в нашем приложении сцену, загружающую несколько CompositeSprite. В sendMessage определены обработчики событий.

IntroTitle.h:

#ifndef _INTROTITLE_H_
#define _INTROTITLE_H_

#include <string.h>
#include "CompositeSprite.h"

class IntroTitle: public CompositeSprite {
    public:
     IntroTitle(Scene* scene, int zOrder): CompositeSprite(scene, 0, 0, zOrder) {}
     virtual bool init();
     virtual void refresh();
};

#endif    // _INTROTITLE_H_

IntroTitle.cpp:

#include "IntroTitle.h"
#include "Sprite.h"

bool IntroTitle::init() {
    if (!AbstractScreenObject::init()) return false;
    // Sprite settings
    setXY(122, 100);
    // Sprite components
    new Sprite(this, "sprite.png", 0, 0, 1);
    return true;
}

void IntroTitle::refresh() {
    CompositeSprite::refresh();
}


Здесь, мы просто выводим надпись с названием игры. Можно было-бы вывести ее отдельным спрайтом, но на случай если к надписи придется что-то добавить, использован CompositeSprite.

IntroMenu.h:

#ifndef _INTROMENU_H_
#define _INTROMENU_H_

#include "CompositeSprite.h"
#include "Button.h"

class IntroMenu: public CompositeSprite {
    private:
        Scene* scene;
        Button* firstButton;
        Button* secondButton;
    public:
        IntroMenu(Scene* scene, int zOrder): CompositeSprite(scene, 0, 0, zOrder), 
                                             scene(scene) {}
        virtual bool init();
};

#endif    // _INTROMENU_H_
IntroMenu.cpp:

#include "IntroMenu.h"
#include "Locale.h"
#include "Intro.h"
#include "Desktop.h"

bool IntroMenu::init() {
    if (!AbstractScreenObject::init()) return false;
    setXY(297, 384);
    firstButton = new Button(this, "play", 0, 0, 1, 
                             Locale::getCurrentImageLocale());
    firstButton->addReceiver(eimPlay, scene);
    secondButton = new Button(this, "setup", 0, 157, 2, 
                              Locale::getCurrentImageLocale());
    secondButton->addReceiver(eimSettings, scene);
    return true;
}


В главном меню определяем две кнопки. Первая будет передавать в сцену событие eimPlay, на обработку которого пока ничего не повешено, вторая сформирует eimSettings, в обработчике которого главное меню делается невидимым и включается меню настроек.

IntroSound.h:

#ifndef _INTROSOUND_H_
#define _INTROSOUND_H_

#include "CompositeSprite.h"

class IntroSound: public CompositeSprite {
    private:
        Scene* scene;
    public:
       IntroSound(Scene* scene, int zOrder): CompositeSprite(scene, 0, 0, zOrder), 
                                             scene(scene) {}
      virtual bool init();
};

#endif    // _INTROSOUND_H_

IntroSound.cpp:

#include "IntroSound.h"
#include "SwitchButton.h"
#include "Button.h"
#include "Intro.h"
#include "Locale.h"

bool IntroSound::init() {
    if (!AbstractScreenObject::init()) return false;
    setXY(346, 227);
    SwitchButton* s = new SwitchButton(this, 0, 0, 1);
        s->addImage("musicoff", 0, Locale::getCurrentImageLocale());
        s->addImage("musicon", 1, Locale::getCurrentImageLocale());
        s->setName("musicon");
        s->setState(1);
    s->addReceiver(eimCheckMusic, scene);
    s = new SwitchButton(this, 0, 157, 2);
        s->addImage("soundoff", 0, Locale::getCurrentImageLocale());
        s->addImage("soundon", 1, Locale::getCurrentImageLocale());
        s->setName("soundon");
        s->setState(1);
    Button* b = new Button(this, "back.png", -300, 350, 3, 
                           Locale::getCommonImageLocale());
    b->addReceiver(eimBack, scene);
    return true;
}


В этом меню, мы определяем кнопки "musicon" и "soundon" управляющие проигрыванием фоновой музыки и звуковых эффектов соответственно.
Теперь, все готово, чтобы запустить приложение. После запуска, видим следующую картинку:



Язык интерфейса определяется в Locale.cpp из настроек оборудования. Кнопки нажимаются и при нажатии на вторую кнопку, происходит переключение в меню настроек:


Здесь, при нажатии на кнопки, меняется надпись "вкл"/"выкл" (соответсвенно включается и выключается фоновая музыка и звуковые эффекты). При нажатии на стрелку, происходит переключение на главное меню.

Как обычно, исходный текст проекта можно скачать здесь.

4 комментария:

  1. Здравствуйте! Очень классный у вас фреймворк получился, правда матрицу и вектора пришлось поменять с "S" на "F". Хотелось бы с вами юолее предметно пообщаться по мармеладу))

    ОтветитьУдалить
    Ответы
    1. Я рад, что вам пригодилось, но боюсь, что в последнее время я несколько отошел от разработки под Android :( Да и лицензия на Мармелад кончилась. Пообщаться я всегда за, если конечно что-то вспомню

      Удалить
    2. сейчас раздают бесплатные годичные лицензии, посмотрите на сайте! Если вам удобно пишите на ящик, если нет на ВКонтакте - astorreviola.

      Удалить
    3. Бесплатные лицензии гляну, спасибо. Ящик в вашем профиле не нашел, а ВК не посещаю по религиозным соображениям (я вообще товарищ антисоциальный). Мне писать можно на glukkazan@gmail.com

      Удалить