返回博客首页
← 所有文章

NativeScript 共享元素过渡 (第 2 部分) - 音乐播放器

2023 年 4 月 3 日 — 作者:技术指导委员会 (TSC)

让我们一起感受音乐的节奏吧?

考虑应用程序交互中的视觉关系有助于提供愉悦的用户体验。这一切都始于屏幕的布局。

在 StackBlitz 上亲自尝试

我们将使用核心的“原生 TypeScript”风格进行说明,但这适用于所有风格。

设置音乐播放器页面 A

在布局第一个页面时,我们知道我们希望专辑图片移动到它在第二个页面上的位置,同时播放按钮也移动到第二个页面上的位置。

布局标记

我们可以如下表示 -- 省略 css 细节以专注于布局标记结构本身 - 请参阅 上面的 StackBlitz 示例,其中包含 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>

我们从 GridLayout 开始,因为它是一个通用的布局容器,可用于定位行/列,也可用于像 Photoshop 文件一样分层视图(一个在另一个之上)。

我们在整个布局的底部分层了我们的主要背景图片。然后,我们通过另一个 GridLayout 在顶部放置一些按行组织的内容。

按照 GridLayout 文档,我们用一个 4 行网格表示通用布局,其中前 2 行(标题和副标题)自动填充其自身高度的每一行。然后,我们在第 3 行放置一个 ListView,用于我们的曲目列表,以占用标题和副标题之后直至包含“迷你播放器”的底行之前的整个可用空间。

声明 sharedTransitionTag

我们知道我们想要在专辑图片和播放按钮上进行一些相关的视觉移动,因此我们用 sharedTransitionTag 值声明它们

<!-- 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" />

设置我们的 SharedTransition

使用 SharedTransition API,我们可以自定义我们希望共享元素过渡发生的方式。

从文档中,我们可以看到只有 iOS 支持“模态”过渡,而页面过渡则由两者都支持。有趣的是,我们可以使用我们的选项配置内容使其看起来像相同的过渡。

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),
  });
}
}

设置音乐播放器页面 B

在布局第二个页面时,我们只需考虑我们设计中的相关元素,同时以我们想要的方式对其进行布局。

<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>

我们在新布局中的专辑和播放按钮都标记了相同的 sharedTransitionTag 值。共享元素过渡将找到匹配的标记值,并在页面 A 和 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" />

非常棒。

使用 SharedTransition 事件

我们可以在页面 B 上连接事件,以便在过渡完成后在我们的专辑后面显示阴影。

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);
  }
);

这些事件可用于实现一些非常有创意的过渡处理效果。我们在 ContentView 上连接了一个 loaded 事件监听器,它纯粹充当一个样式元素,用于在我们的专辑图片后面放置阴影/辉光。然后,我们允许 SharedTransition 事件驱动专辑阴影显示与否。

总结

此示例说明了 @nativescript/core 中共享元素过渡在 iOS 和 Android 上的多功能性,使您可以获得视觉上令人惊叹的效果。

有一些创意想法可以实现真正引人入胜的视觉效果?与社区分享并在 Twitter 上使用 #nstricks 进行标记,我们期待看到您创作出的美丽作品!