Back to Blog Home
← all posts

NativeScript Shared Element Transitions (Part 2) - Music Player

April 3, 2023 — by Technical Steering Committee (TSC)

Let's groove to some music 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 Music Player Page A

When laying out this first page, we know we want the album image to move into it's place on the second page while the play button moves into position on the second page as well.

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>
    <Image src="~/assets/Weeknd.jpg" stretch="aspectFill" row="0" iosOverflowSafeArea="true"></Image>
    <GridLayout rows="auto, auto, *, auto" row="0">
        <Label text="The Weeknd" class="music-player-title" row="0" col="1" textWrap="true" width="50%" />
        <Label text="1,453,233 Monthly Listeners" row="1" col="1" textWrap="true" width="50%" />

        <ListView items="{{ items }}" row="2" separatorColor="transparent">
            <ListView.itemTemplate>
                <StackLayout orientation="horizontal" height="68">
                    <Label text="{{ number }}" />
                    <StackLayout orientation="vertical">
                        <Label text="{{ title }}" />
                        <Label text="{{ description }}" />
                    </StackLayout>
                </StackLayout>
            </ListView.itemTemplate>
        </ListView>
        <GridLayout columns="auto,auto,*" height="65" row="3" tap="{{openSongPlayerView}}">
            <Image src="~/assets/Album.jpg" sharedTransitionTag="album" stretch="aspectFill" width="66" height="66"/>
            <StackLayout col="1">
                <Label text="Starboy" />
                <Label text="The Weeknd, Daft Punk" />
            </StackLayout>
            <Image src="~/assets/PlayButton.png" sharedTransitionTag="play-button" col="2" stretch="aspectFill" width="25" height="25" />
        </GridLayout>
    </GridLayout>
</GridLayout>

We start with a GridLayout because it's a versatile layout container that can be used to position rows/columns but also can be used to layer views like a Photoshop file (one on top of the other).

We have our main background image layered at the bottom of the entire layout. Then we place some content organized in rows atop via another GridLayout.

Following the GridLayout docs, we represent the general layout with a 4 row grid where the first 2 rows (title and subtitle) auto fill each row with their own height. We then place a ListView in the 3rd row for our track listing to take up the entire available space after our title and subtitle and up until the bottom row containing our "mini player".

Declare sharedTransitionTag

We know we want some relational visual movement on the album image and the play button so we declare them with sharedTransitionTag values:

<!-- enable transition on the album image -->
<Image src="~/assets/Album.jpg" sharedTransitionTag="album" stretch="aspectFill" width="66" height="66" />

<!-- enable transition on the play button -->
<Image src="~/assets/PlayButton.png" sharedTransitionTag="play-button" col="2" stretch="aspectFill" width="25" height="25" />

Setup our SharedTransition

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

From the docs, we can see that only iOS supports "modal" transitions where page transitions are supported by both. What's interesting here is that we can configure things to look like the same transition with our options.

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

// setup modal open for iOS and page navigation for Android to show a new "player-view" we will layout in a moment
const moduleName = 'player-view';
const config: SharedTransitionConfig = {
  // (iOS only) - allows interactively dragging to dismiss
  interactive: {
    dismiss: {
      finishThreshold: 0.5,
    },
  },
  pageStart: {
    opacity: 1,
    x: 0,
    // For Android, start at the bottom of the view (height of device).
    // This will allow the transition to look like a modal pop-up.
    // A good practice on Android is to use heightPixels
    // Whereas on iOS, we often use heightDIPs for animations
    // (so we omit to rely on defaults here for iOS)
    y: isAndroid ? Screen.mainScreen.heightPixels : null,
  },
  pageEnd: {
    // (iOS only) - customize spring settings
    spring: { tension: 70, friction: 9, mass: 1 },
  },
  pageReturn: {
    opacity: 1,
    // (iOS only) - customize spring settings
    spring: { tension: 70, friction: 9, mass: 2 },
  },
};
if (isIOS) {
  // Since iOS supports modals, we can open via modal 
  const option: ShowModalOptions = {
    context: {},
    closeCallback: () => {},
    fullscreen: true,
    transition: SharedTransition.custom(new ModalTransition(), config),
  };
  this.page.showModal(moduleName, option);
} else {
  // on Android we can just navigate with our options (which will look like a modal open)
  this.page.frame.navigate({
    moduleName,
    transition: SharedTransition.custom(new PageTransition(), config),
  });
}
}

Setup Music Player 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="auto,*">
  <!-- album shadow-->
  <ContentView width="300" height="300" backgroundColor="black" loaded="{{loadedAlbumBg}}" opacity="0">
  </ContentView>
  <!-- album -->
  <Image src="~/assets/Album.jpg" sharedTransitionTag="album" stretch="aspectFit" width="300" height="300" />

  <StackLayout row="1">
    <Label text="Starboy" />
    <Label text="The Weeknd, Daft Punk" />
    <Slider value="10" minValue="0" maxValue="100">
    </Slider>
    <GridLayout columns="auto,*,auto">
      <Label col="0" text="2:47" />
      <Label col="2" text="3:50" />
    </GridLayout>
    <GridLayout columns="*,auto,auto,auto,*" height="400">
      <Image col="1" src="~/assets/PreviousSong.png" stretch="aspectFill" width="22" height="22" />
      <Image col="2" src="~/assets/PlayButton.png" sharedTransitionTag="play-button" stretch="aspectFill" width="66" height="66" />
      <Image col="3" src="~/assets/NextSong.png" stretch="aspectFill" width="22" height="22" />
    </GridLayout>
  </StackLayout>

  <Image src="~/assets/close.png" stretch="aspectFill" tap="{{close}}" color="white" width="20" height="20" rowSpan="2" verticalAlignment="top" marginLeft="20" />
</GridLayout>

We tag the same sharedTransitionTag values for both the album and the play button in this new layout. The shared element transition will find the matching tagged values and auto animate between their positions on both Page A and B.

<!-- album -->
<Image src="~/assets/Album.jpg" sharedTransitionTag="album" stretch="aspectFit" width="300" height="300" />

<!-- play button -->
<Image col="2" src="~/assets/PlayButton.png" sharedTransitionTag="play-button" stretch="aspectFill" width="66" height="66" />

Pretty neat.

Use SharedTransition events

We can wire up events on Page B to allow a shadow to show up behind our album when the transition is complete.

SharedTransition.events().on(SharedTransition.startedEvent, (event) => {
  if (['dismiss', 'interactiveStart'].includes(event.data.action)) {
    this.toggleAlbumBg(false);
  }
});
SharedTransition.events().on(SharedTransition.finishedEvent, (event) => {
  if (event.data.action === 'present') {
    this.toggleAlbumBg(true);
  }
});
SharedTransition.events().on(
  SharedTransition.interactiveCancelledEvent,
  () => {
    this.toggleAlbumBg(true);
  }
);

These events can be used to achieve some very creative effects with transition handling. We wire up a loaded event listener on a ContentView which serves as purely a styling element to place a shadow/glow behind our album image. We then allow the SharedTransition events to drive when the album shadow displays or not.

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!