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:
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.
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.
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.
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.
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.
- Zu beachten
- Tipp: Natürlich ist die Verwendung von
Animationen
undTimelines
auch dann möglich, wenn die Button-State-Umschaltung überNodeId
oderChildIndex
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 dieResource::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:
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.
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 AttributenenableContainerFitting="true"
,containerSizeX
undcontainerSizeY
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.
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.
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.
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:
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.
NinePatchPlaneSequenceGeometry
Wenn sich die Grafik in einem Atlas befindet, kann der NinePatchPlaneSequenceGeometry
-Knoten zum Rendern verwendet werden.
Die XmlAtlas
-Datei erstellen wir mit dem Atlas Generator (siehe auch Tutorial #02: Atlas Demo). Dabei setzen wir in der Konfigurationsdatei für AtlasXML
das Attribut createNames="TRUE"
, damit wir die Grafik später einfach mit dem Namen referenzieren können.
<AtlasGenerator xmlns="http://murlengine.com"> <Input path="../data/orig/ui_skin"> <Matte color="0i, 0i, 0i"/> <Crop cropThreshold="1i" /> <Image names="*"/> </Input> <Output path="../data/packages/main.murlres"> <Atlas sizeRaster="2"/> <Image name="gfx_gui.png" margin="8"/> <AtlasXML name="atlas_gui.xml" createNames="TRUE" textureSlots="1" materialSlot="4"/> </Output> </AtlasGenerator>
Die erzeugte XmlAtlas
-Datei enthält dann Rectangles
mit Namen für alle Images im Atlas:
<?xml version="1.0" encoding="utf-8"?> <!-- Auto created by atlas_generator --> <Atlas xmlns="http://murlengine.com"> ... <Rectangle name="button_primary" materialSlot="4" textureSlots="1" coordSizeX="224" coordSizeY="84" texCoordX1="0.8681640625" texCoordY1="0" texCoordX2="0.9775390625" texCoordY2="0.08203125"/> <Rectangle name="button_secondary" materialSlot="4" textureSlots="1" coordSizeX="218" coordSizeY="82" texCoordX1="0.5078125" texCoordY1="0.6640625" texCoordX2="0.6142578125" texCoordY2="0.744140625"/> ... </Atlas>
Das Rectangle
referenzieren wir mit den Attribut atlasResourceId
und rectangleName
. Mit den Atrributen capTexCoordSizeX1/X2/Y1/Y2
können wiederum die Teilungs-Achsen festgelegt werden. Auf eine Angabe der Attribute capCoordSizeX1/X2/Y1/Y2
kann verzichtet werden, wenn die Werte identisch zu den Werten für capTexCoordSizeX1/X2/Y1/Y2
sind.
<NinePatchPlaneSequenceGeometry id="button_primary_ninepatch" atlasResourceId="package_main:atlas_gui" rectangleName="button_primary" frameSizeX="500" frameSizeY="84" capTexCoordSizeX1="36" capTexCoordSizeX2="36" capTexCoordSizeY1="0" capTexCoordSizeY2="0" />