Tutorial #10: Buttons

Mit dem Knoten Graph::Button können einfache Steuerelemente für die Interaktion mit dem Benutzer erstellt werden. Dabei enthält der Graphenknoten Button selbst keine sichtbaren Elemente, sondern definiert lediglich einen Bereich in der Szene, der auf Mausklicks oder Touch-Ereignisse reagieren kann.

Version 1: Einfacher Button

Button Node

Wir beginnen mit einem Default-Projekt und definieren in der XML-Datei graph_main.xml einen Graph::Button Knoten mit id="button01". Mit den Parametern sizeX und sizeY definieren wir die Größe des Button und mit posX, posY und posZ die Position. Der Button enthält keine Geometrie und ist daher unsichtbar. Um den Buttonbereich sichtbar zu machen, definieren wir an derselben Position und mit derselben Größe einen Graph::PlaneGeometry Knoten. Das Ganze packen wir noch in einen gemeinsamen Graph::Transform Knoten mit id="transform01".

<?xml version="1.0" ?>

<Graph>
  <Instance graphResourceId="package_main:graph_mat"/>
  <Instance graphResourceId="package_main:graph_camera"/>
  <MaterialState materialId="material/mat_color_alpha"/>
  <FixedParameters id="par_color01" diffuseColor="149i, 185i, 200i, 128i"/>
  <ParametersState parametersId="par_color01"/>

  <Transform id="transform01">
    <Button id="button01"
      sizeX="200" sizeY="100"
      posX="0" posY="0" posZ="0"
    />
    <PlaneGeometry id="plane01"
      scaleFactorX="200" scaleFactorY="100"
      posX="0" posY="0" posZ="0"
    />
  </Transform>
</Graph>

Ereignissteuerung

Im Programmcode benötigen wir eine Membervariable vom Typ Logic::ButtonNode, um auf Buttonereignisse reagieren zu können. Zusätzlich definieren wir drei weitere Membervariablen, um den Button im Raum drehen und verschieben zu können.

    Murl::Logic::ButtonNode mButton01;
    Murl::Logic::TransformNode mTransform01;
    Double mAngle, mZPos;

Die neuen Variablen werden im Konstruktor bzw. in der OnInit Methoded initialisiert.

    App::ButtonsLogic::ButtonsLogic(Logic::IFactory* factory)
    : BaseProcessor(factory),
    mAngle(0),
    mZPos(0)
    {
    }
    ...
    Bool App::ButtonsLogic::OnInit(const Logic::IState* state)
    {
        state->GetLoader()->UnloadPackage("startup");
        
        Graph::IRoot* root = state->GetGraphRoot();
        AddGraphNode(mButton01.GetReference(root, "button01"));
        AddGraphNode(mTransform01.GetReference(root, "transform01"));

        if (!AreGraphNodesValid())
        {
            return false;
        }
        
        state->SetUserDebugMessage("Buttons Init succeeded!");
        return true;
    }
    ...

Nun können wir mit der Variable mButton01 den Button bei jedem Tick auf etwaige Ereignisänderungen abfragen. Wir geben einfach bei jedem "Button-Press" die aktuelle Ticktime als User-Debug-Message aus.

    void App::ButtonsLogic::OnProcessTick(const Logic::IState* state)
    {
        Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler();
        //Double tickDuration = state->GetRecentTickDuration();

        // Exit
        if (deviceHandler->WasRawKeyPressed(RAWKEY_ESCAPE) ||
            deviceHandler->WasRawButtonPressed(RAWBUTTON_BACK))
        {
            deviceHandler->TerminateApp();
        }
        // Button01
        if (mButton01->WasReleasedInside())
        {
          state->SetUserDebugMessage("Button pressed "+Util::DoubleToString(state->GetCurrentTickTime()));
          
          const Vector& pos = mButton01->GetEventPosition();
          Debug::Trace(Util::DoubleToString(pos.x) + "/" + Util::DoubleToString(pos.y) + "/" + Util::DoubleToString(pos.z));
        }
    }

Zusätzlich können wir die Position an der der "Button-Press" erfolgte als Debug-Meldung ausgeben. Die Positionswerte erhalten wir von der Methode GetEventPosition. Die Werte entsprechen der lokalen Position am Button, wobei der Wert 0/0 genau im Zentrum des Buttons liegt. In der Vorderansicht sind die Werte für rechts/oben positiv und für links/unten negativ.

Für unseren Button mit einer Größe von 200x100 gilt:

tut0110_button_area.png
Buttonkoordinaten Frontansicht

Auch wenn der Button im Raum transformiert wird und nicht genau 200 Pixel groß ist, werden immer die korrekten Werte geliefert. Bei einer 180° Drehung um die Y-Achse (Rückansicht) ändern sich die Werte für rechts/oben auf -100/50 und für links/unten auf 100/-50.

tut0110_button_area_rotated.png
Buttonkoordinaten Schrägansicht

Zum besseren Verständnis erweitern wir den Code, um den Button im Raum drehen und verschieben zu können. Mit den Tasten F1/F2 soll die Z-Position des Buttons verändert werden und mit den Tasten F3/F4 soll der Button rotiert werden. Die Taste R (Reset) soll den Anfangszustand wiederherstellen.

    void App::ButtonsLogic::OnProcessTick(const Logic::IState* state)
    {
        Logic::IDeviceHandler* deviceHandler = state->GetDeviceHandler();

        if (deviceHandler->IsRawKeyPressed(RAWKEY_R))
        {
          mAngle = 0;
          mZPos = 0;
          mTransform01->SetPositionZ(mZPos);
          mTransform01->SetRotation(0, mAngle, 0);
        }

        if (deviceHandler->IsRawKeyPressed(RAWKEY_F1))
        {
          mZPos += 10;
          mTransform01->SetPositionZ(mZPos);
        }
        if (deviceHandler->IsRawKeyPressed(RAWKEY_F2))
        {
          mZPos -= 10;
          mTransform01->SetPositionZ(mZPos);
        }

        if (deviceHandler->IsRawKeyPressed(RAWKEY_F3))
        {
          mAngle += 0.05;
          mAngle = Math::Fmod(mAngle, Math::TWO_PI);
          mTransform01->SetRotation(0, mAngle, 0);
        }
        if (deviceHandler->IsRawKeyPressed(RAWKEY_F4))
        {
          mAngle -= 0.05;
          mAngle = Math::Fmod(mAngle, Math::TWO_PI);
          mTransform01->SetRotation(0, mAngle, 0);
        }
        ...

Wird der Button um mehr als 90 Grad gedreht, fällt auf, dass die Rückseite des Buttons nicht reagiert. Mit dem Attribut activeFaces kann dieses Verhalten auf z.B. FRONT_AND_BACK geändert werden.

Gültige Werte sind:

  • FRONT_ONLY (default)
  • BACK_ONLY
  • FRONT_AND_BACK
  • NONE

Standardmäßig reagiert der Button nur auf die linke Maustaste. Mit dem Attribut mouseButton kann auf eine andere Maustaste umgeschaltet werden.

Gültige Werte sind:

  • LEFT (default)
  • RIGHT
  • MIDDLE
  • NEXT
  • PREV
  • NONE

Auf Buttonereignisse des Touch-Geräts wird unabhängig vom gesetzten mouseButton Wert immer reagiert.

tut0110_v1.png
V1: Einfacher Button

Version 2: Erweiterter Button

Als nächstes erweitern wir den Button mit passenden Grafiken. Dabei sollen die verschiedenen Buttonzustände auch unterschiedlich visualisiert werden.

Buttonzustand

Der Button-Knoten beinhaltet wie schon erwähnt keine Geometrie und hat daher kein Aussehen. Das optische Erscheinungsbild kann mit beliebigen Knotenelementen passend zur Anwendung gestaltet werden.

Buttons unterstützen das automatische Umschalten des Aussehens für unterschiedliche Buttonzustände. Dabei muss für jeden Buttonzustand (upState, downState, hoverState, disabledState) ein Knoten festgelegt und über Attributwerte dem Button-Knoten bekannt gemacht werden. Der Button-Knoten aktiviert je nach Buttonzustand den richtigen Knoten und ändert damit das Aussehen des Buttons automatisch.

Zu beachten
Achtung! Jeder Zustand benötigt einen eigenen Knoten, da bei einem Zustandswechsel dieser Knoten aktiviert und alle anderen deaktiviert werden. Die Verwendung von einem Knoten für zwei Zustände ist nicht möglich. Sehr wohl ist es aber möglich zwei Referenzen eines Knotens für zwei Zustände zu verwenden.

Die Zuordnung von Visualisierungsknoten und dem Buttonknoten kann entweder über den mit 0 beginnenden Reihenfolgenindex der Kind-Elemente oder über den eindeutigen Namen (Knoten-ID) erfolgen. Alternativ besteht noch die Möglichkeit die Visualisierung der Zustandsänderung über eine Timeline zu definieren.

Button Ressourcen

Für unser Beispiel erstellen wir Grafiken eines runden Powerbuttons, wobei unterschiedliche Farben die unterschiedlichen States visualisieren sollen. Mithilfe des Tools Atlas Generator erstellen wir aus den Einzelgrafiken einen Textur Atlas sowie eine Planes-XML-Datei. Die genaue Verwendung des Atlas Generator Tools wird im Tutorial #02: Atlas Demo genauer beschrieben. An dieser Stelle sei nur darauf hingewiesen, dass durch Aufruf des Scripts scripts/atlas_generator aus den Einzelgrafiken im Verzeichnis data/orig ein Texturatlas im Verzeichnis data/packages/main.murlres/gfx erzeugt wird.

data/packages/main.murlres/gfx/gfx_powerbutton.png
data/packages/main.murlres/gfx/planes_powerbutton.xml

Wir machen die beiden neuen Dateien in der Datei package.xml bekannt.

<Resource id="gfx_powerbutton" fileName="gfx/gfx_powerbutton.png"/>
<Resource id="planes_powerbutton" fileName="gfx/planes_powerbutton.xml"/>

In der Datei graph_materials.xml erstellen wir einen Graph::FlatTexture Knoten und instanzieren den Subgraph der in der Planes-XML-Datei definiert ist.

<FlatTexture
  id="tex_powerbutton"
  imageResourceId="package_main:gfx_powerbutton"
  pixelFormat="R8_G8_B8_A8"
  useMipMaps="false"
/>

<Instance graphResourceId="package_main:planes_powerbutton"/>

Da der Material-Namespace-Knoten das Attribut activeAndVisible="no" gesetzt hat, werden dessen Kind-Knoten beim Zeichnen nicht abgearbeitet und die instanzierten PlaneGeometry-Knoten der Planes-XML-Datei werden auch nicht gezeichnet.

Um die Button Grafiken zu Zeichnen, verwenden wir Graph::Reference Knoten. Ein Reference-Knoten ist ein Verweis auf einen anderen Knoten im Szenengraph. Beim Durchlaufen des Szenengraphen wird diesem Verweis gefolgt, sofern der Reference-Knoten aktiv ist. Reference-Knoten sind leichtgewichtig und verbrauchen nur sehr wenig Speicher. Daher sind Reference-Knoten mehreren Instance-Knoten immer vorzuziehen, sofern das im konkreten Anwendungsfall möglich ist.

Button-State-Zuordnung über Kind-Index

Eine Zuordnung über den Kind-Index erfolgt mit den Attributen:

  • upStateChildIndex
  • downStateChildIndex
  • hoverStateChildIndex
  • disabledStateChildIndex

Dabei müssen die Visualisierungselemente als direkte Kind-Elemente des Buttonknoten definiert werden. Die Auswahl erfolgt über den mit 0 beginnenden Reihenfolgenindex der Kind-Knoten.

Für unseren Powerbutton hängen wir dem Button-Knoten vier Referenzknoten als Kindknoten an und definieren mit den ChildIndex-Attributen welche Grafik bei welchem State angezeigt werden soll. Natürlich müssen wir auch dafür sorgen, dass zuvor das richtige Material und die richtige Textur aktiviert wurden.

  <MaterialState materialId="material/mat_alpha_texture"/>
  <TextureState textureId="material/tex_powerbutton"/>

  <Transform id="transform01">
    <Button id="button01" sizeX="182" sizeY="182" shape="ELLIPSE"
      upStateChildIndex="0" downStateChildIndex="1"
      hoverStateChildIndex="2" disabledStateChildIndex="3"
    >
      <Reference targetId="material/powerbutton-up"/>
      <Reference targetId="material/powerbutton-down"/>
      <Reference targetId="material/powerbutton-hover"/>
      <Reference targetId="material/powerbutton-disabled"/>
    </Button>
  </Transform>

Da unsere Button-Grafik nicht rechteckig sondern rund ist, passen wir noch die Form des Buttons mit shape="ELLIPSE" an. Der Standard-Parameter für das Attribut shape ist "RECTANGLE".

Als Ergebnis erhalten wir einen runden Button, der abhängig vom Button-Zustand seine Farbe ändert.

tut0110_v2a.png
Power Button

Button-State-Zuordnung über Knoten-ID

Eine Zuordnung über die Knoten-ID erfolgt analog mit den Attributen:

  • upStateNodeId
  • downStateNodeId
  • hoverStateNodeId
  • disabledStateNodeId

Dabei können beliebige Knoten im Szenenzgraphen verwendet werden – es müssen also keine direkten Kind-Knoten des Button-Knotens sein.

  <Transform id="transform02" posX="-200">
    <Reference id="up" targetId="material/powerbutton-up"/>
    <Reference id="down" targetId="material/powerbutton-down"/>
    <Reference id="hover" targetId="material/powerbutton-hover"/>
    <Reference id="disabled" targetId="material/powerbutton-disabled"/>
    <Button id="button02" sizeX="182" sizeY="182" shape="ELLIPSE"
      upStateNodeId="up" downStateNodeId="down"
      hoverStateNodeId="hover" disabledStateNodeId="disabled"
    />
  </Transform>

Button-State-Zuordnung über Timeline

Eine Visualisierung mit einer Timeline erfolgt mit den Attributen:

  • timelineId
  • timelineIndex
  • upStateTime
  • downStateTime
  • hoverStateTime
  • disabledStateTime

Der Timeline-Knoten kann wieder entweder mit der Knoten-Id und dem Attribut timelineId oder mit dem Reihenfolgenindex der Kind-Knoten und dem Attribut timelineIndex definiert werden. Mit den StateTime-Attributen können die EndTime-Werte für die jeweiligen Zustände definiert werden. Bei einer Zustandsänderung des Buttons, wird die Timeline auf die angegebene EndTime eingestellt und die Timeline gestartet.

Zur Veranschaulichung erzeugen wir einen Button, der die Zustandsänderungen über die Größe visualisiert. Dafür erstellen wir zuerst eine neue XMLAnimation-Ressource-Datei.

<?xml version="1.0" ?>

<Animation>
    <!-- Button animation -->
    <ScalingKey time="0.00" scaleX="0.10" scaleY="0.10" scaleZ="1.0" interpolation="HERMITE_EASE_IN_OUT"/>
    <ScalingKey time="0.50" scaleX="1.00" scaleY="1.00" scaleZ="1.0" interpolation="HERMITE_EASE_IN_OUT"/>
    <ScalingKey time="0.60" scaleX="1.20" scaleY="1.20" scaleZ="1.0" interpolation="HERMITE_EASE_IN_OUT"/>
    <ScalingKey time="0.70" scaleX="0.00" scaleY="0.00" scaleZ="1.0"/>
</Animation>

Dem Button-Knoten fügen wir einen Timeline-Knoten hinzu, der seinerseits über einen Scale-Knoten und unsere Animation die Anzeige des Button-Elements steuert. Ein Scale-Knoten skaliert einfach seinen Subgraphen, ähnlich wie ein Transform-Knoten seinen Sub-Graphen transformiert (verschiebt/rotiert).

  <Transform id="transform03  " posX="200">
    <Button id="button03" sizeX="182" sizeY="182" shape="ELLIPSE"
      timelineIndex="0"
      upStateTime="0.5" hoverStateTime="0.55"
      downStateTime="0.6" disabledStateTime="0.7"
    >
      <Timeline>
        <Scale controller.animationResourceId="package_main:anim_button">
          <Reference targetId="material/powerbutton-up"/>
        </Scale>
      </Timeline>
    </Button>
  </Transform>

Als Ergebnis erhalten wir einen dritten Button, der mit animierten Größenänderungen auf Maus- und Touch-Ereignisse reagiert.

Zu beachten
Achtung! Wenn zwei Buttons übereinander liegen, werden nur Events des obersten Buttons gemeldet. Dieses Verhalten kann mit dem Paramter passEvents="true" geändert werden, sodass auch Events des darunterliegenden Buttons gemeldet werden. Zum Testen kann der mittlere Button verschoben werden. Die Pfeiltasten ändern die Bildschirmposition, die Tasten F1/F2 ändern die z-Ebene.
tut0110_v2b.png
V2: Erweiterter Button
Zu beachten
Tipp: Natürlich ist die Verwendung von Animationen und Timelines auch dann möglich, wenn die Button-State-Umschaltung über NodeId oder ChildIndex erfolgt. Beispiel:
  <Transform id="transform01">
    <Button id="button01" sizeX="362"  sizeY="364" upStateChildIndex="0" downStateChildIndex="1" hoverStateChildIndex="2" disabledStateChildIndex="3">
      <!-- Up state -->
      <Reference targetId="material/powerbutton-up"/>
      <!-- Down state -->
      <Timeline>
        <Scale controller.animationResourceId="package_main:anim_button">
          <Reference targetId="material/powerbutton-up"/>
        </Scale>
      </Timeline>
      <!-- Hover state -->
      <Reference targetId="material/powerbutton-hover"/>
      <!-- Disabled state -->
      <Reference targetId="material/powerbutton-disabled"/>
    </Button>
  </Transform>

Version 3: Toggle-Button & Multi-Touch-Events

Toggle-Button

Ein Toggle Button ist ein Button, der unabhängig von den Button-States zwischen zwei Zuständen umschalten kann. Er kann also entweder eingeschalten oder ausgeschalten sein.

Da der Ein- und Ausschaltzustand auch mit einem anderen Aussehen visualisiert werden soll, muss je nach Zustand auch die PlaneGeometry des Button umgeschalten werden. Am einfachsten kann das mit einem Graph::Switch-Knoten bewerkstelligt werden.

Ein Switch-Knoten aktiviert keinen oder genau einen seiner direkten Kind-Knoten und ignoriert alle anderen Kind-Knoten. Der aktive Kind-Knoten kann über den Reihenfolgenindex (Attribut index) oder über den ID-Namen (Attribut selectedChildId) gesetzt werden.

Für unseren Toggle Button definieren wir einen Switch-Knoten, der zwischen powerbutton-disabled und powerbutton-up umschalten kann. Für die unterschiedlichen Button-States referenzieren wir immer diesen Switch-Knoten. Damit der Switch-Knoten nicht doppelt gezeichnet wird, müssen wir diesen noch in einen Knoten mit visible="false" Attribut verpacken.

  <Transform id="transform01" posX="-259" posY="-159">
    <Button id="button" sizeX="182" sizeY="182" shape="ELLIPSE"
      upStateChildIndex="1" downStateChildIndex="2"
      hoverStateChildIndex="3" disabledStateChildIndex="4"
    >
      <Node visible="false">
        <Switch id="buttonState" index="0" >
          <Reference targetId="material/powerbutton-disabled"/>
          <Reference targetId="material/powerbutton-up"/>
        </Switch>
      </Node>
      <Reference targetId="buttonState"/>
      <Scale scaleFactor="0.9">
        <Reference targetId="buttonState"/>
      </Scale>
      <Reference targetId="buttonState"/>
      <Reference targetId="material/powerbutton-disabled"/>
    </Button>
  </Transform>

In der Logik-Klasse definieren wir eine Membervariable vom Typ Logic::SwitchNode und schalten mit mButtonSwitch->SetIndex() den Button-State um.

    // Toggle-Button
    if (mButton->WasReleasedInside())
    {
        if (mButtonSwitch->GetIndex() != 0) 
        {
            mButtonSwitch->SetIndex(0);
            state->SetUserDebugMessage("switched off");
        }
        else
        {
            mButtonSwitch->SetIndex(1);
            state->SetUserDebugMessage("switched on");
        }
    }

Als Ergebnis erhalten wir einen Toggle Button, der per Maus-Klick bzw. Touch-Event ein und ausgeschalten werden kann. Dabei kann der aktuelle Zustand des Buttons jederzeit über mButtonSwitch->GetIndex() abgefragt werden.

Multi-Touch-Events

Ein Button-Knoten kann nicht nur für simple Buttons, sondern auch für komplexere Multi-Touch Gestensteuerungen verwendet werden. Als einfaches Beispiel zeigen wir ein Bild an und erlauben das Zoomen und Verschieben mit Maus und Multi-Touch-Eingabegeräten.

Als Anzeigebild verwenden wird das Bild Interstitia-andy-gilmore.jpg vom New Yorker Künstler Andy Gilmore. Es hat eine Auflösung von 1280 x 1478 Pixel. Das ist offensichtlich keine Power-of-Two Auflösung und genau das kann bei Geräten mit älteren Grafikchips zu Problemen führen. Daher erstellen wir mit einem Grafikprogramm eine skalierte Version mit einer gültigen Power-of-Two Auflösung. In unserem Fall Interstitia-andy-gilmore-1024.jpg mit einer Auflösung von 1024x1024.

In der Datei package.xml definieren wir beide Bilddateien als Grafik-Ressource und verwenden je nach Feature-Verfügbarkeit das passende Bild. Die Fähigkeit des Grafikchipsatzes mit nicht Power-of-Two Grafiken richtig umzugehen kann mit includeForFeatures="FULL_NON_POWER_OF_TWO_TEXTURES" abgefragt werden.

Zu beachten
Tipp: Eine Liste aller möglichen includeForX Bedingungen findet sich in der API Dokumentation für die Resource::Object Klasse.
  <Resource id="gfx_picture" fileName="gfx/Interstitia-andy-gilmore.jpg" includeForFeatures="FULL_NON_POWER_OF_TWO_TEXTURES"/>
  <Resource id="gfx_picture" fileName="gfx/Interstitia-andy-gilmore-1024.jpg"/>

Auf diese Art können abhängig von den aktuell verfügbaren Features unterschiedliche Ressourcen für eine bestimmte id geladen werden. Es wird immer die erste Ressource, bei der alle geforderten Features vorhanden sind, genommen und alle nachfolgenden Ressourcen mit gleicher id werden übersprungen. Die einzelnen Features, die abgefragt werden können, sind im Enum IEnums::Feature aufgelistet.

Einzelne Features können natürlich auch kombiniert werden. Beispiel:

<!-- Feature 1 and Feature 2 must be available to load gfx_1.png-->
<Resource id="gfx" fileName="gfx_1.png" includeForFeatures="1" includeForFeatures="2">
<!-- Feature 2 or Feature 3 must be available to load gfx_2.png-->
<Resource id="gfx" fileName="gfx_2.png" includeForFeatures="3">
<Resource id="gfx" fileName="gfx_2.png" includeForFeatures="4">
<!-- Fallback is gfx_3.png-->
<Resource id="gfx" fileName="gfx_3.png">

In der Datei graph_materials.xml erstellen wir einen FlatTexture Knoten für die Grafik-Ressource.

    <FlatTexture
      id="tex_gfx"
      imageResourceId="package_main:gfx_picture"
      pixelFormat="R8_G8_B8_A8"
      useMipMaps="false"
    />

Für die Anzeige des Bildes verwenden wir eine PlaneGeometry der Größe 434x500; für die Steuerung einen Button-Knoten derselben Größe. Den PlaneGeometry-Knoten verpacken wir noch in einen Switch-Knoten, damit wir die Anzeige mit dem bereits erstellten Toggle-Button ein- und ausschalten können.

  <MaterialState materialId="material/mat_texture"/>
  <TextureState textureId="material/tex_gfx"/>

    <Switch id="gfxState">
      <PlaneGeometry id="gfx"
        scaleFactorX="434" scaleFactorY="500"
        texCoordX1="0" texCoordY1="0"
        texCoordX2="1" texCoordY2="1"
        posX="100"
    />
    </Switch>

    <Button id="gfxButton" posX="100" sizeX="434" sizeY="500"/>

Der Graph::Button Knoten bietet zahlreiche Methoden für die Verarbeitung von Multi-Touch-Eingaben.

Die Methode GetNumberOfTrackedEvents() gibt an, wie viele Touch-Events derzeit nachverfolgt werden (Anzahl der Finger auf dem Touch-Gerät). Dabei kann mit dem Attribut maxNumberOfEvents oder mit der Methode SetMaxNumberOfEvents(value) die maximale Anzahl der gemeldeten Touch-Events (Finger) begrenzt werden.

Mit der Methode GetTrackedEventId(trackedEventIndex) erhält man eine zugehörige, eindeutige id für jeden aktuell anliegenden Event.

Die Methoden GetLocalEventPosition(id) bzw. GetEventOutCoord(id) liefern passend zur angegebenen id die zugehörigen Touch Positionswerte.

Die Werte von GetLocalEventPosition(id) entsprechen der lokalen Position am Button, wobei der Wert 0/0 genau im Zentrum des Buttons liegt (analog zu GetEventPosition). GetEventOutCoord(id) liefert Werte zwischen -1 und +1.

Für unser Beispiel mit einem Button der Größe von 434 x 500 liefert GetLocalEventPosition die folgende Werte:

tut0110_button_event_position.png
Button Event Position

In der Header Datei deklarieren wir uns passende Membervariablen und Methoden. Mit dem Enum GfxState und der Variable mGfxState erstellen wir eine einfache State-Maschine, um zwischen Leerlauf, Verschieben und Zoomen umschalten zu können. Die Variablen mGfxCoordX1, mGfxCoordY1, mGfxCoordX2, mGfxCoordY2 dienen der Speicherung des aktuell angezeigten Bereichs. In der Map mTrackedEventPositions speichern wir die Koordinaten der Events zusammen mit der Event id.

    enum GfxState
    {
        STATE_IDLE = 0,
        STATE_DRAGGING,
        STATE_ZOOMING,
    };
    void SetGfxCoords(Double x1, Double y1, Double x2, Double y2);
    void ZoomGfx(Double zoomFactor);
    void DoIdle();
    void DoDrag();
    void DoZoom();

    GfxState mGfxState;
    Double mGfxCoordX1, mGfxCoordX2, mGfxCoordY1, mGfxCoordY2;
    Map<UInt32, Graph::Vector> mTrackedEventPositions;

    Murl::Logic::ButtonNode mButton;
    Murl::Logic::SwitchNode mButtonSwitch;

    Murl::Logic::PlaneGeometryNode mGfx;
    Murl::Logic::SwitchNode mGfxSwitch;
    Murl::Logic::ButtonNode mGfxButton;

Die Double Membervariablen werden im Konstruktor initialisiert.

App::ButtonsLogic::ButtonsLogic(Logic::IFactory* factory)
: BaseProcessor(factory),
mGfxState(STATE_IDLE),
mGfxCoordX1(0),
mGfxCoordY1(0),
mGfxCoordX2(1),
mGfxCoordY2(1)
{
}

In der Methode OnInit wird eine Referenz auf die Graphenknoten erstellt.

    AddGraphNode(mGfx.GetReference(root, "gfx"));
    AddGraphNode(mGfxButton.GetReference(root, "gfxButton"));
    AddGraphNode(mGfxSwitch.GetReference(root, "gfxState"));

In der Methode OnProcessTick verwenden wir den Toggle-Button um das angezeigte Bild ein- und auszuschalten. Zusätzlich resetten wir mit SetGfxCoords(0,0,1,1) den Anzeigebereich beim Einschalten auf einen definierten Bereich.

    // Toggle-Button
    if (mButton->WasReleasedInside())
    {
        if (mButtonSwitch->GetIndex() != 0) 
        {
            mButtonSwitch->SetIndex(0);
            mGfxSwitch->SetIndex(-1);
            state->SetUserDebugMessage("switched off");
        }
        else
        {
            mButtonSwitch->SetIndex(1);
            mGfxSwitch->SetIndex(0);
            SetGfxCoords(0,0,1,1);
            state->SetUserDebugMessage("switched on");
        }
    }

Optional erlauben wir das Zoomen auch mit dem Mausrad. Die Werteänderungen des Mausrads liefert die Methode GetRawWheelDelta.

    // Mouse-Wheel
    if (deviceHandler->IsMouseAvailable())
    {
        Graph::Real deltaX = 0;
        Graph::Real deltaY = 0;
        deviceHandler->GetRawWheelDelta(deltaX, deltaY);
        if (deltaY != 0)
        {
            ZoomGfx(deltaY);
        }
    }

Als letztes rufen wir bei jedem ProcessTick noch abhängig von der Zustandsvariable mGfxState die entsprechende Methode auf.

  // Switch gfx States
  switch (mGfxState)
  {
  case STATE_IDLE:
      DoIdle();
      break;
  case STATE_DRAGGING:
      DoDrag();
      break;
  case STATE_ZOOMING:
      DoZoom();
      break;
  default:
      break;
  }

Im Zustand STATE_IDLE wird lediglich geprüft wieviele Button-Touch-Events existieren. Bei einem Event wird in den Zustand STATE_DRAGGING gewechselt, bei zwei oder mehr Events wird in den Zustand STATE_ZOOMING gewechselt. Das Umschalten des States erfolgt allerdings erst dann, wenn zumindest ein Finger um mehr als 16 Einheiten verschoben wurde (Totzone). Dafür speichern wir die Positionswerte für jeden Event in mTrackedEventPositions und vergleichen diese mit den aktuallen Positionswerten.

void App::ButtonsLogic::DoIdle()
{
    // remove stored but no longer tracked ids
    for (SInt32 i = 0; i < mTrackedEventPositions.GetCount(); i++)
    {
        if (!mGfxButton->IsEventTracked(mTrackedEventPositions.GetKey(i)))
        {
            mTrackedEventPositions.Remove(i);
            i--;
        }
    }

    // add new ids or check distance of existing ids
    UInt32 numEvents = mGfxButton->GetNumberOfTrackedEvents();
    for (UInt32 i = 0; i < numEvents; i++)
    {
        UInt32 id = mGfxButton->GetTrackedEventId(i);
        SInt32 index = mTrackedEventPositions.Find(id);

        if (index < 0)
        {
            mTrackedEventPositions.Add(id, mGfxButton->GetLocalEventPosition(id));
        }
        else
        {
            Graph::Vector diff = mGfxButton->GetLocalEventPosition(id) - mTrackedEventPositions[index];
            if (diff.GetLength() > Real(16.0))
            {
                if (numEvents == 1)
                    mGfxState = STATE_DRAGGING;
                else
                    mGfxState = STATE_ZOOMING;
            }
        }
    }
}

Im Zustand STATE_DRAGGING werden die Positionswerte des Events ausgelesen und der Anzeigebereich wird entsprechend verschoben. Der Zustand ändert sich auf STATE_IDLE, wenn die Anzahl der anliegenden Touchevents ungleich 1 ist.

void App::ButtonsLogic::DoDrag()
{
    // drag ends
    if (mGfxButton->GetNumberOfTrackedEvents() != 1)
    {
        mTrackedEventPositions.Empty();
        mGfxState = STATE_IDLE;
        return;
    }

    UInt32 id = mGfxButton->GetTrackedEventId(0);
    const Graph::Vector& pos = mGfxButton->GetLocalEventPosition(id);
    Double deltaX = (mTrackedEventPositions[0].x - pos.x) * (mGfxCoordX2 - mGfxCoordX1) / 434;
    Double deltaY = (mTrackedEventPositions[0].y - pos.y) * (mGfxCoordY2 - mGfxCoordY1) / 500;
    SetGfxCoords(mGfxCoordX1 + deltaX, mGfxCoordY1 - deltaY, mGfxCoordX2 + deltaX,    mGfxCoordY2 - deltaY);
    mTrackedEventPositions[0] = pos;
}

Analog dazu werden im Zustand STATE_ZOOMING die Positionswerte der ersten beiden Events ausgelesen und daraus ein Zoomfaktor berechnet, der dann der Methode ZoomGfx übergeben wird. Der Zustand ändert sich auf STATE_IDLE, wenn die Anzahl der anliegenden Touchevents kleiner als 2 wird.

void App::ButtonsLogic::DoZoom()
{
    // zoom ends
    if (mGfxButton->GetNumberOfTrackedEvents() < 2)
    {
        mTrackedEventPositions.Empty();
        mGfxState = STATE_IDLE;
        return;
    }
    
    Graph::Vector oldDiff = mTrackedEventPositions[0] - mTrackedEventPositions[1];

    UInt32 id0 = mGfxButton->GetTrackedEventId(0);
    mTrackedEventPositions[0] = mGfxButton->GetLocalEventPosition(id0);
    UInt32 id1 = mGfxButton->GetTrackedEventId(1);
    mTrackedEventPositions[1] = mGfxButton->GetLocalEventPosition(id1);
    Graph::Vector newDiff = mTrackedEventPositions[0] - mTrackedEventPositions[1];
    ZoomGfx((newDiff.GetLength()-oldDiff.GetLength())/100);
}

Die ZoomGfx Methode berechnet aus dem aktuellen Anzeigebereich den Fenstermittelpunkt und die Fenstergröße. Diese Werte werden dann mit dem zoomFactor entsprechend vergrößert oder verkleinert. Dabei wird mit der Methode Math::Clamp sicher gestellt, dass die Werte für den Anzeigebereich immer in einem definierten Bereich liegen.

Zu beachten
In dieser Implementierung wird immer um den Anzeigemittelpunkt gezoomt und nicht um den Mittelpunkt der beiden Touch-Events.
void App::ButtonsLogic::ZoomGfx(Double zoomFactor)
{
  Double scx = (mGfxCoordX1 + mGfxCoordX2) * 0.5;
  Double scy = (mGfxCoordY1 + mGfxCoordY2) * 0.5;
  Double scaleFactorX = Math::Abs(mGfxCoordX2 - mGfxCoordX1);
  Double newSize = scaleFactorX*(1-zoomFactor*0.2)/2;
  newSize = Math::Clamp(newSize, 0.05, 10.0);
  SetGfxCoords(scx - newSize, scy - newSize, scx + newSize, scy + newSize);
}

Als Ergebnis erhalten wir einen Toggle-Button und eine Bildanzeige, die mit Maus und Multi-Touch verschoben, vergrößert und verkleinert werden kann.

tut0110_v3.png
V3: Toggle Button & Bildanzeige

Version 4: Text-Button & 9-Slice-Scaling

Als letztes Beispiel wollen wir einen einfachen, wiederverwendbaren Button mit Text erstellen.

Text-Button

Wie schon in Version 2: Erweiterter Button erstellen wir uns verschiedene Grafiken für die verschiedenen Button-States, erzeugen mit dem Atlas Generator einen Texturatlas und legen die generierten Dateien im Unterverzeichnis main.murlres/button ab.

Als nächstes erzeugen wir einen Subgraphen in der Datei graph_init_button.xml. Dieser Subgraph legt einmal alle notwendigen Materialen und Planes an. Alle Buttons können diese Knoten dann einfach referenzieren.

Die einzelnen Knoten verpacken wir in den Namespace ressource_button. Die textCoord-Werte für die PlaneGeometry-Knoten übernehmen wir von der generierten Datei planes_button.xml. Die Datei planes_button.xml wird ansonsten nicht benötigt und kann auch gelöscht werden.

<?xml version="1.0"?>

<Graph materialId="/material/mat_alpha_texture" imageResourceId="package_main:gfx_button">
  <Namespace id="ressource_button" activeAndVisible="no">
    <FlatTexture
          id="tex"
          imageResourceId="{imageResourceId}"
          pixelFormat="R8_G8_B8_A8"
          useMipMaps="false"
    />
    <Node id="mat">
      <MaterialState materialId="{materialId}"/>
      <TextureState textureId="tex"/>
    </Node>
    <PlaneGeometry id="button_disabled" depthOrder="10" texCoordX1="0.000000000000" texCoordX2="0.414062500000" texCoordY1="0.000000000000" texCoordY2="0.281250000000"/>
    <PlaneGeometry id="button_down"     depthOrder="10" texCoordX1="0.000000000000" texCoordX2="0.414062500000" texCoordY1="0.281250000000" texCoordY2="0.562500000000"/>
    <PlaneGeometry id="button_hover"    depthOrder="10" texCoordX1="0.000000000000" texCoordX2="0.414062500000" texCoordY1="0.562500000000" texCoordY2="0.843750000000"/>
    <PlaneGeometry id="button_up"       depthOrder="10" texCoordX1="0.414062500000" texCoordX2="0.828125000000" texCoordY1="0.000000000000" texCoordY2="0.281250000000"/>
  </Namespace>
</Graph>

Den Subgraph für den Button definieren wir in der Datei graph_button.xml. Es wird zuerst das passende Material gesetzt, und danach der Button gezeichnet. Für die Textanzeige verwenden wir einen TextGeometry-Knoten.

Zu beachten
Tipp: Die Textgröße des TextGeometry-Knoten kann automatisch angepasst werden. Ohne Anpassung steht ein langer Text möglicherweise über die Buttongrafik hinaus. Ist dies nicht gewünscht, kann mit den Attributen enableContainerFitting="true", containerSizeX und containerSizeY eine automatische Anpassung erreicht werden.

Alle Knoten werden wiederum in einen eigenen Namespace gepackt, damit es bei den vergebenen id-Namen zu keinen Konflikten kommen kann (diese müssen ja bekanntlich eindeutig sein). Zusätzlich sorgt der SubState-Knoten dafür, dass das geänderte Material wieder auf die ursprünglichen Werte zurück gestellt wird.

<?xml version="1.0" ?>

<Graph posX="0" posY="0" posZ="0" axisX="0" axisY="1" axisZ="0" angle="0 deg"
       sizeX="210" sizeY="70" passEvents="no" shape="RECTANGLE" fontSize="24" text="Button">
  <Namespace id="{buttonId}">
    <SubState>
      <Reference targetId="/ressource_button/mat"/>
      <Transform posX="{posX}" posY="{posY}" posZ="{posZ}" axisX="{axisX}" axisY="{axisY}" axisZ="{axisZ}" angle="{angle}">
        <!-- Button -->
        <Scale scaleFactorX="{sizeX}" scaleFactorY="{sizeY}">
          <Button id="button"
                  shape="{shape}"
                  passEvents="{passEvents}"
                  upStateChildIndex="0" downStateChildIndex="1" hoverStateChildIndex="2" disabledStateChildIndex="3" >
            <Reference targetId="/ressource_button/button_up"/>
            <Reference targetId="/ressource_button/button_down"/>
            <Reference targetId="/ressource_button/button_hover"/>
            <Reference targetId="/ressource_button/button_disabled"/>
          </Button>
        </Scale>
        <!-- Text Geometry -->
        <TextGeometry id="text"
          depthOrder="11"
          systemFontName="SansBold"
          fontSize="{fontSize}"
          textColor="255i, 255i, 255i, 255i"
          backgroundColor="0i, 0i, 0i, 0i"
          text="{text}"
        />
      </Transform>
    </SubState>
  </Namespace>
</Graph>

Die Dateien werden in der Package-Definition bekannt gemacht.

  <!-- Button resources -->
  <Resource id="gfx_button" fileName="button/gfx_button.png"/>
  <Resource id="init_button" fileName="button/graph_init_button.xml"/>
  <Resource id="graph_button" fileName="button/graph_button.xml"/>

In der Datei graph_main.xml können dann mehrerer Buttons einfach instanziert werden.

  <!--Create Button Ressource Instance-->
  <Instance graphResourceId="package_main:init_button"/>
  
  <!--Draw Buttons-->
  <Instance graphResourceId="package_main:graph_button" buttonId="button01" posX="-100"/>
  <Instance graphResourceId="package_main:graph_button" buttonId="button02" posX="-100" posY="150" sizeY="30" text="Button 2"/>
  <Instance graphResourceId="package_main:graph_button" buttonId="button03" posX="-100" posY="-150" sizeX="70" sizeY="70" text="Btn3"/>

Als Ergebnis erhalten wir wie gewünscht drei Buttons am Bildschirm.

tut0110_v4a.png
Text Buttons

Mit dieser Implementierung werden beim Skalieren des Buttons auch die Dicke der Randlinie und der Radius mitskaliert. Wenn dies nicht gewünscht ist, kann die 9-Patch Technik (auch 9-Slice Technik) weiterhelfen.

9-Patch Text-Button

Bei der 9-Patch Technik wird eine Grafik durch zwei vertikale und zwei horizontale Achsen in 9 Teile unterteilt. Für die Skalierung werden die Teile 1, 3, 7, 9 gar nicht, die Teile 4, 6 nur vertikal und die Teile 2, 8 nur horizontal skaliert. Der Teil 5 wird horizontal und vertikal skaliert. Dadurch bleiben die Dicke von Randlinien und die Proportionen von Eckteilen erhalten.

tut0110_nine_patch.png
9-Patch Technik

Für die 9-Patch Technik gibt es einen eigenen Graph::NinePatchPlaneGeometry Knoten. Die Attribute capTexCoordSizeX1, capTexCoordSizeY1, capTexCoordSizeX2, capTexCoordSizeY2 legen die Achsenposition für die Unterteilung der Textur fest. Die Attribute capCoordSizeX1, capCoordSizeY1, capCoordSizeX2, capCoordSizeY2 legen die Achsenposition für die Unterteilung der Plane fest. Die einzelnen Textur-Teile werden dann auf die entsprechenden Plane-Teile gemappt.

tut0110_nine_patch_mapping.png
9-Patch capCoord Attribute

Für den Button erstellen wir eine 32x32 Pixel große Grafik. Die Teilungs-Achsen sollen jeweils 15 Pixel vom Rand entfernt liegen. Mit einem 1:1 Mapping zwischen Textur und Plane sieht unser Graph::NinePatchPlaneGeometry Knoten dann folgendermaßen aus.

<NinePatchPlaneGeometry id="geometry"
  depthOrder="10"
  frameSizeX="210" frameSizeY="70"

  textureSizeX="32" textureSizeY="32"
  texCoordX1="0" texCoordY1="0"
  texCoordX2="32" texCoordY2="32"
  capTexCoordSizeX1="15" capTexCoordSizeX2="15"
  capTexCoordSizeY1="15" capTexCoordSizeY2="15"

  capCoordSizeX1="15" capCoordSizeX2="15"
  capCoordSizeY1="15" capCoordSizeY2="15"
/>

Die Attribute textureSizeX und textureSizeY definieren einen Wertebereich von 0 - 32 für X und Y an Stelle des standardmäßigen Wertebereichs von 0 – 1. Die Attribute texCoordX/Y/1/2 legen den zu verwendenden Texturausschnitt fest. In unserem Fall wird die gesamte Textur verwendet. Die Attribute capTexCoordSizeX/Y/1/2 legen die Teilungs-Achsen für die Textur fest. Die Attribute capCoordSizeX/Y/1/2 legen die Teilungs-Achsen für die Plane fest. Da die Werte gleich sind, erhalten wir ein pixelgenaues 1:1 Mapping für die Eckteile.

Wir könnten uns auch die Angabe der Attribute textureSizeX/Y und texCoordX/Y/1/2 sparen und die capTexCoordSizeX/Y/1/2 Werte bezogen auf den Werte Bereich 0 – 1 direkt angeben:

15/32 = 0.46875
  capTexCoordSizeX1="0.46875" capTexCoordSizeX2="0.46875"
  capTexCoordSizeY1="0.46875" capTexCoordSizeY2="0.46875"

Anstatt wie vorher für jeden Buttonzustand eine eigene Grafik zu definieren, können wir die Zustandsänderung natürlich auch mit geänderten Farbparametern erzielen. Unser Button Subgraph sieht dann wie folgt aus.

<?xml version="1.0" ?>

<Graph posX="0" posY="0" posZ="0" axisX="0" axisY="1" axisZ="0" angle="0 deg"
       sizeX="210" sizeY="70" passEvents="no" shape="RECTANGLE" fontSize="24" text="9S-Button">
  <Namespace id="{buttonId}">
    <SubState>
      <!-- Material -->
      <Reference targetId="/ressource_button_9s/mat"/>
      <!-- Geometry -->
      <Node activeAndVisible="no">
        <NinePatchPlaneGeometry id="geometry"
          depthOrder="10"
          frameSizeX="{sizeX}" frameSizeY="{sizeY}"

          textureSizeX="32" textureSizeY="32"
          texCoordX1="0" texCoordY1="0"
          texCoordX2="32" texCoordY2="32"
          capTexCoordSizeX1="15" capTexCoordSizeX2="15"
          capTexCoordSizeY1="15" capTexCoordSizeY2="15"

          capCoordSizeX1="15" capCoordSizeX2="15"
          capCoordSizeY1="15" capCoordSizeY2="15"
          allowDynamicBatching="yes"
        />
      </Node>
      <Transform posX="{posX}" posY="{posY}" posZ="{posZ}" axisX="{axisX}" axisY="{axisY}" axisZ="{axisZ}" angle="{angle}">
        <!-- Button -->
        <Button id="button"
                sizeX="{sizeX}" sizeY="{sizeY}"
                shape="{shape}"
                passEvents="{passEvents}"
                upStateChildIndex="0" downStateChildIndex="1" hoverStateChildIndex="2" disabledStateChildIndex="3" >
          <Node>
            <ParametersState parametersId="/ressource_button_9s/param_button_up"/>
            <Reference targetId="geometry"/>
          </Node>
          <Node>
            <ParametersState parametersId="/ressource_button_9s/param_button_down"/>
            <Reference targetId="geometry"/>
          </Node>
          <Node>
            <ParametersState parametersId="/ressource_button_9s/param_button_hover"/>
            <Reference targetId="geometry"/>
          </Node>
          <Node>
            <ParametersState parametersId="/ressource_button_9s/param_button_disabled"/>
            <Reference targetId="geometry"/>
          </Node>
          <!-- Text Geometry -->
          <TextGeometry id="text"
            depthOrder="11"
            systemFontName="SansBold"
            fontSize="{fontSize}"
            textColor="255i, 255i, 255i, 255i"
            backgroundColor="0i, 0i, 0i, 0i"
            text="{text}"
          />
        </Button>
      </Transform>
    </SubState>
  </Namespace>
</Graph>

Die referenzierten Parameter werden wieder in einer für alle Instanzen gemeinsamen Ressourcendatei definiert:

<?xml version="1.0"?>

<Graph materialId="/material/mat_alpha_color_texture" imageResourceId="package_main:gfx_button_9s">
  <Namespace id="ressource_button_9s" activeAndVisible="no">
    <FlatTexture
          id="tex"
          imageResourceId="{imageResourceId}"
          pixelFormat="R8_G8_B8_A8"
          useMipMaps="false"
    />
    <Node id="mat">
      <MaterialState materialId="{materialId}"/>
      <TextureState textureId="tex"/>
    </Node>
    
    <FixedParameters id="param_button_up"       diffuseColor="D0ffffffh"/>
    <FixedParameters id="param_button_down"     diffuseColor="80ffffffh"/>
    <FixedParameters id="param_button_hover" />
    <FixedParameters id="param_button_disabled" diffuseColor="40ffffffh"/>

  </Namespace>
</Graph>

In der Datei graph_main.xml können dann wieder mehrerer Buttons instanziert werden.

  <!--Create Button Ressource Instance-->
  <Instance graphResourceId="package_main:init_button_9s"/>

  <!--Draw Buttons-->
  <Instance graphResourceId="package_main:graph_button_9s" buttonId="9sbutton01" posX="200"/>
  <Instance graphResourceId="package_main:graph_button_9s" buttonId="9sbutton02" posX="200" posY="150" sizeY="30" text="9S-Button 2"/>
  <Instance graphResourceId="package_main:graph_button_9s" buttonId="9sbutton03" posX="200" posY="-150" sizeX="70" sizeY="70" text="9S-3"/>

Als Ergebnis erhalten wir drei zusätzliche Buttons, die schön mit der 9-Patch Technik skalieren.

tut0110_v4b.png
V4: Text-Buttons


Copyright © 2011-2018 Spraylight GmbH.