Back to Blog Home
← all posts

NativeScript Shared Element Transitions (Part 1) - Space Man

March 28, 2023 — by Technical Steering Committee (TSC)

Let's travel to Space shall we?

Thinking about visual relation in your apps interaction can help deliver delightful user experiences. It all starts in the layout of your screens.

Try it for yourself on StackBlitz

We'll be using the "Vanilla TypeScript" flavor of core to illustrate but this is applicable to all flavors.

Setup Space Man Page A

When laying out this first page, we know we want the earth to move into it's place on the second page while the Space Man's visor comes straight at us, revealing the settling of the second page.

Layout the markup

We can represent this as follows -- omitting css details to focus on layout markup structure alone - see StackBlitz example above with css

<GridLayout rows="3*, 2*">
  <image
    src="~/assets/Earth.png"
    sharedTransitionTag="earth"
    width="75%"
    stretch="aspectFit"
  ></image>
  <image
    src="~/assets/SpaceMan.png"
    sharedTransitionTag="spaceman"
    stretch="aspectFill"
  ></image>
  <GridLayout rows="160,auto,auto,auto,*" sharedTransitionTag="title">
    <label row="1" text="SPACE" />
    <label row="2" text="TRAVEL" />
    <button row="3" text="TICKETS AVAILABLE" width="55%" tap="{{ open }}" />
  </GridLayout>
  <GridLayout
    row="1"
    rows="auto,*"
    columns="auto,*"
    sharedTransitionTag="infobox"
  >
    <ContentView width="150" height="100">
      <image
        src="~/assets/Astronaut.jpg"
        width="150"
        height="100"
        stretch="aspectFill"
      ></image>
    </ContentView>
    <StackLayout col="1">
      <label text="98% RATING" />
      <label text="FIND OUT MORE" />
    </StackLayout>
    <label row="1" colSpan="2" text="{{descriptionText}}" textWrap="true" />
  </GridLayout>
</GridLayout>

Following the GridLayout docs, we represent the general layout with a 2 row grid where 1 row takes up 3 times the available space, 3*, and the remaining 2 times the available space, 2*.

In the first row, we position our earth image below the spaceman image (layered like a photoshop file in this case) and our "Space Travel" text with the button layered on top of that.

In the second row, we have a detailed description area.

Declare sharedTransitionTag

We know we want some relational visual movement on the Earth and SpaceMan images so we declare them with sharedTransitionTag values:

<image
  src="~/assets/Earth.png"
  sharedTransitionTag="earth"
  width="75%"
  stretch="aspectFit"
></image>
<image
  src="~/assets/SpaceMan.png"
  sharedTransitionTag="spaceman"
  stretch="aspectFill"
></image>

We also want the "Space Travel" title and "Tickets Available" button to move together out to the left when revealing the next page, so we include that altogether in it's own layout container we can declare sharedTransitionTag on as well:

<GridLayout rows="160,auto,auto,auto,*" sharedTransitionTag="title">
  <label row="1" text="SPACE" />
  <label row="2" text="TRAVEL" />
  <button row="3" text="TICKETS AVAILABLE" width="55%" tap="{{ open }}" />
</GridLayout>

Lastly, we'd like the detailed description area to move down and out of the way so we place sharedTransitionTag on it too:

<GridLayout
  row="1"
  rows="auto,*"
  columns="auto,*"
  sharedTransitionTag="infobox"
></GridLayout>

Setup our SharedTransition on the page navigation

With the SharedTransition API we can customize how we want our Shared Element Transition to occur.

import { SharedTransition } from '@nativescript/core';

// setup page navigation to move to a new "earth-view" we will layout in a moment
page.frame.navigate({
  moduleName: 'earth-view',

  // we define our transition here
  transition: SharedTransition.custom(new PageTransition(), {
    // where the page we're moving away from should end up
    pageEnd: {
      // on iOS, we can define "independent" tag movement
      sharedTransitionTags: {
        // fade spaceman out while moving and scaling up
        spaceman: {
          opacity: 0,
          y: 20,
          scale: {
            x: 6,
            y: 6,
          },
        },
        // fade title out while moving off to the left
        title: {
          opacity: 0,
          x: -200,
        },
        // fade infobox out while moving it down and out
        infobox: {
          opacity: 0,
          y: 800,
        },
      },
    },
    // how the page returns back
    pageReturn: {
      // we use duration to override default spring settings
      // in this example, looks better with linear duration animation
      duration: 600,
    },
  }),
});

Setup Space Man Page B

When laying out the second page, we just consider the relational elements in our design while laying it out however we'd like.

<GridLayout rows="*, *, *, *">
  <GridLayout columns="auto,auto">
    <image
      src="~/assets/back.png"
      tap="{{close}}"
      stretch="aspectFill"
      width="20"
      height="20"
    />
    <button col="1" text="Back" tap="{{close}}" width="50" />
  </GridLayout>
  <GridLayout row="1" rowSpan="2" rows="auto,auto,auto">
    <label text="EARTH" />
    <label
      row="1"
      text="Earth is the third planet from the Sun and the only object in the universe known to harbor life."
      textWrap="true"
      width="70%"
    />
    <image
      row="2"
      src="~/assets/Moon.png"
      stretch="aspectFit"
      width="80"
      height="80"
    />
  </GridLayout>
  <GridLayout rows="*" row="2" rowSpan="2">
    <image
      src="~/assets/Earth.png"
      stretch="aspectFit"
      sharedTransitionTag="earth"
    />
  </GridLayout>
</GridLayout>

What is perhaps most interesting in this example is that we are only declaring 1 view with sharedTransitionTag="earth" since it exists on both Page A and Page B. Doing so allows the earth to fluidly transition from it's position on Page A to it's new larger position on Page B.

The rest of the animation is actually covered by the SharedTransition.custom options provided, which for iOS, defines various sharedTransitionTag elements on Page A alone to animate according to those options.

So if iOS supports "independent" tagged elements only present on one page and not the other, how to handle Android?

Note: Android "independent" tagged elements will be supported in future core release.

The solution is actually quite simple. You can include inside a <android> section of your markup the view layout that you want Android to create relation with. So we can simply do that for the section that contains the other sharedTransitionTag's we want animated, eg:

<android>
  <GridLayout rows="3*, 2*" rowSpan="4">
    <image
      src="~/assets/SpaceMan.png"
      stretch="aspectFill"
      sharedTransitionTag="spaceman"
      scaleX="8"
      scaleY="8"
      translateY="940"
    ></image></GridLayout
></android>

On Android, when the page is navigated, it will now find those relational sharedTransitionTag's to animated to the positions their view declares.

Summary

This example illustrates the versatility of Shared Element Transitions in @nativescript/core for iOS and Android allowing you to achieve visually stunning results.

Have some creative ideas for really engaging visuals? Share with the community and tag on Twitter with #nstricks and we look forward to seeing the beautiful creations you come up with!