Keine Zeit? Hier geht’s zum Code. Hier geht’s zum Video.

Fast Track: Find the code here. Find the video here.

Ich bin mir sicher, jeder von Euch kenn den Parallax Scrolling oder 3D Scrolling Effekt, der in unterschiedlichen Apps, Spielen und sogar auf dem Windows 8.1 Startscreen im Hintergrund ausgeführt wird. Dabei verändern sich Elemente im Hintergrund, wenn der Anwender den Vordergrund scrollt. Im Falle von Windows 8 wird also beispielsweise auf dem Startmenü gescrollt, die Kacheln bewegen sich nach links. Im gleichen Moment bewegt sich der Bildschirmhintergrund. In Spielen wird mit einem ähnlichen Effekt häufig erzielt, dass sich die Landschaft mehrdimensionaler anfühlt, manchmal wird dieser Effekt auch einfach genutzt um das UI etwas liebevoller zu gestalten.

 

In Windows 8.1 Preview ist das neue HubControl eingeführt worden, das es uns ermöglich Inhalte aus unterschiedlichen Kategorien auf sehr einfache Art in unsere App einzubinden. Natürlich profitiert so ein Hub wahnsinnig von einem clever gewählten Hintergrundbild. Der Wunsch nach einem Parallax Scrolling Effekt liegt da nicht fern. Ich hab versucht diesen Effekt nachzubilden – letzlich benötigen wir ja nur folgenden Schritt:

Wann immer die Scrollposition des Hubs sich ändert, verschieben wir die Hintergrund Elemente.

Das Verschieben der Hintergrund Element passiert typischerweise in einer anderen Geschwindigkeit als bei den Elementen im Vordergrund. Man kann hier also noch beliebig viel Aufwand in Berechnungen stecken um besonders ansprechende grafische Effekte zu erzielen. Hierauf soll es aber in diesem Posting nicht ankommen.

Die wesentlich spannendere Frage ist, wie ich eigentlich die Scrollposition des Hubs erfahre. Sieht man sich das HubControl einmal näher an, wird man herausfinden, dass es kein eigenes Property für die Scrollposition gibt. Über einen kleinen Trick kommen wir aber trotzdem an die Information die wir benötigen.

Der Trick

Folgende Zeilen Code liefern uns das gewünschte Ergebnis:

public double GetXScrollOffset(UIElement rootVisual)

        {

            Point relativePoint = myFirstHubSection.TransformToVisual(rootVisual).TransformPoint(new Point(0, 0));

            return relativePoint.X;           

        }

Was passiert hier? Wir lassen uns über die Position eines bestimmten Elements relativ zum übergeordneten Element (rootVisual) geben. In meinem Fall habe ich die Funktion so eingebaut, dass ich die Position der ersten HubSection relativ zum Hub ausgeben lasse. Das ganze rufe ich immer dann auf, wenn mein Hub den LayoutUpdated Event feuert. Anschließend bewege ich die Elemente im Hintergrund abhängig von den errechneten Werten:

private void MyHub_LayoutUpdated(object sender, object e)

        {

            var offset = GetXScrollOffset(MyHub);

            ShiftElements(offset);

        }

 

Ich hab das Ergebnis mal in einem Video festgehalten. Ich denke, das Ergebnis zeigt ganz gut, was möglich ist. Die Ruckler im Video kommen übrigens durch die Aufnahme zustande. Probiert es einfach aus, es läuft sehr smooth. (Man beachte die grandiose handwerkliche Kunst bei den Grafiken!) Ihr findet das Video hier:

k7Cts3uGS3o

 

Falls Ihr Euch gleich auf den Quellcode stürzen wollt, den habe ich auch abgelegt und zwar hier auf meinem Skydrive

Hier noch einmal die wesentlichen Schritte, Schritt für Schritt erklärt.

HubControl Template

Die  Basis für das gesamte Projekt ist das HubControl Template aus Visual Studio 2013 Preview. Ihr bekommt dann ein paar Beispieldaten geliefert, die für unser Sample ausreichend sind. 

Hintergrund

Um das Hintergrundbild zu positionieren könnte man einfach für das HubControl selbst einen Hintergrund festlegen. Dadurch würde ich mir jedoch die Möglichkeit nehmen auf mehreren Ebenen zu scrollen. Also lege ich das HubControl in ein Grid und lege für dieses Grid einen Hintergrund fest. Dem CompositeTransform Element gebe ich einen Namen um einfach darauf zugreifen zu können. 

<Grid Canvas.ZIndex="0">

        <Grid.Background >

            <ImageBrush   Stretch="UniformToFill"  ImageSource="ms-appx:///images/sf.png">

                <ImageBrush.Transform>

                    <CompositeTransform x:Name="MyGridBackgroundCt"  TranslateX="0"/>

                </ImageBrush.Transform>

            </ImageBrush>

        </Grid.Background>       

        </Hub>

       …

        </Hub>

    </Grid>  

 

Weitere Elemente

Zusätzliche Elemente (im Beispiel Fluggeräte) lege ich auch einfach im Grid ab. Horizontal und Vertical Alignment habe ich so festgelegt, dass ich von der oberen linken Ecke aus rechnen kann. (also Left und Top). Die startposition kann ich über Margins festlegen. Beim Scrollen ändere ich später einfach die Werte für TransformX oder TransformY, passe die Größe der Elemente über Width & Height an oder verändere den Z-Index oder drehe diese mit RotationY um die eigene Achse. 

Scroll Event mitkriegen

Wie oben beschrieben nutze ich den LayoutUpdated Event um mitzukriegen, wann sich die ScrollPosition geändert hat. Dazu wiederum nutze ich die relative Position der ersten HubSection zum Hub. 

private void MyHub_LayoutUpdated(object sender, object e)

        {

            var offset = GetXScrollOffset(MyHub);

            ShiftElements(offset);

        }

 

public double GetXScrollOffset(UIElement rootVisual)

        {

            Point relativePoint = myReferenceHubSection.TransformToVisual(rootVisual).TransformPoint(new Point(0, 0));

            var xScrollOffset = relativePoint.X;

            return xScrollOffset;

        } 

Elemente bewegen

Die Elemente bewege ich in meiner ShiftElements() Methode. Im Beispiel habe ich auf das Arbeiten mit Databindings verzichtet, weil es für unnötigen Overhead gesorgt hätte. Für eine echte Anwendung würde ich Euch das allerdings ans Herz legen. 

ShiftElements arbeitet mit dem ScrollOffset und bewegt daraufhin die Elemente bspw. Veränderungen der TransformX Eigenschaft. Dabei gibt es einiges zu beachten, was jetzt nicht wirklich mit dem Thema zu tun hat, sondern eher mathematischer Natur ist. Nur soviel: Kümmert Euch um das Vorzeichen des ScrollOffsets. J

Da der Hintergrund langsamer scrollt, als der Vordergrund habe ich noch einen Faktor mit einbauen, der das Scrolling verzögert. 

// Scroll Background

var BackgroundMove = xScrollOffset / 5;

MyGridBackgroundCt.TranslateX = BackgroundMove; 

Im Sample habe ich allen Elementen unterschiedliche Faktoren zukommen lassen. Der Zeppelin fliegt im Kreis, also mal nach links, mal nach rechts, immer abhängig von der Scrollposition. Über das Auswerten des Rests der Modulo-Operation (%) lässt sich sowas verwirklichen. So kann man festlegen, dass der Zeppelin immer nur ein paar Pixel nach rechts und dann wieder nach links fliegt, obwohl die Scrollrichtung die gleiche bleibt. 

Außerdem kann man natürlich mit den Ebenen spielen. Ihr werdet im Sample feststellen, dass das Flugzeug hinter der Hub-Ebene startet, aber dann kurz nach vorne kommt und wieder nach hinten fliegt. Hierzu habe ich einfach abhängig von der Position die ZIndex Property auf 99 gesetzt. 

// Come to foreground

MyPlaneImg.SetValue(Canvas.ZIndexProperty, 99);

 

Wenn man dann noch die Größe der Elemente verändern will, um ein Näherkommen zu simulieren, kann man das natürlich über Width/Height machen. Hierzu habe ich auf Basis der Scrollposition einen Resize-Faktor errechnet, der dann die Größe verändert. 

MyPlaneImg.Width = MyPlaneImgStdWidth + (MyPlaneImgStdWidth * ResizeFactor);

MyPlaneImg.Height = MyPlaneImgStdHeight + (MyPlaneImgStdWidth * ResizeFactor); 

Im Prinzip ist dieser Teil aber reine Spielerei und gänzlich Euch überlassen. Und jetzt: Auf zum Parallax Scrolling! Mehr Info zu den neuen Controls unter Windows 8.1 Preview findet Ihr bereits im DevCenter.