Animated Bottom Sheet in NativeScript
By popular demand, in this tutorial, we'll be creating an animated bottom sheet that works on iOS and Android. TL;DR If you prefer watching…
Take control of your career. Build JavaScript mobile apps.
By popular demand, in this tutorial, we'll be creating an animated bottom sheet that works on iOS and Android.
TL;DR
If you prefer watching a video of the tutorial, then here you go:
Introduction
This article is the third part in a series of articles we've posted. We'll be using concepts and code covered in the previous two articles, so you might want to first check them out. The first was on multiple frames and the second on custom events.
In this tutorial, we'll use multiple frames to create the bottom sheet, but this is not the only way that can be achieved. We do however believe that it is the most flexible, reusable and maintainable way do it. If you've implemented a NativeScript bottom sheet before using a different method, we'd love to hear about it. You can let us know in the comments below.
Getting Started
We'll start off with the app created in the multiple frames tutorial.
![Starting app]](starting_app.png)
The app displays two frames. When the GO TO 2
button is tapped, the top frame navigates from Page 1 to Page 2 while the bottom red frame remains in place.
To start off, let's add another button to the home-page.xml
file, which is the default page of the top Frame.
<!-- home-page.xml -->
<Page xmlns="http://schemas.nativescript.org/tns.xsd">
<StackLayout>
<Label text="Page 1" class="page-name" />
<Button text="Go to 2" tap="page1BtnTap" />
<Button text="Show" tap="btnShowTap" />
</StackLayout>
</Page>
You can see both Frames in main-page
:
<!-- main-page.xml -->
<Page xmlns="http://schemas.nativescript.org/tns.xsd" actionBarHidden="true">
<GridLayout>
<Frame defaultPage="home-page">
</Frame>
<Frame defaultPage="action-page" loaded="actionFrameLoaded">
</Frame>
</GridLayout>
</Page>
When the button we added is tapped, we'll make it so that it triggers the bottom sheet to animate and move upwards. Notice that the button is on a different Frame from the bottom Frame. To communicate between different frames, we'll use custom events just as we did in the previous article.
Before doing that, let's first make sure that the bottom Frame is completely positioned below the screen and hidden from view.
// main-page.ts
import { EventData } from "tns-core-modules/data/observable";
import { Page } from "tns-core-modules/ui/page";
import { HelloWorldModel } from "./main-view-model";
import { Frame } from "tns-core-modules/ui/frame/frame";
import { screen, isIOS } from "tns-core-modules/platform";
export function actionFrameLoaded(args: EventData) {
const frame = args.object as Frame;
frame.translateY = screen.mainScreen.heightDIPs;
}
Now when you run the app, the bottom frame won't be visible.
Before implementing the btnShowTap
event handler that will be called when the Show button is tapped, thus triggering the bottom sheet to slide up, let's first register the custom event that will perform this animation:
// main-page.ts
import { EventData } from "tns-core-modules/data/observable";
import { Page } from "tns-core-modules/ui/page";
import { HelloWorldModel } from "./main-view-model";
import { screen, isIOS } from "tns-core-modules/platform";
import { CubicBezierAnimationCurve } from "tns-core-modules/ui/animation/animation";
import * as frameModule from "tns-core-modules/ui//frame";
export function actionFrameLoaded(args: EventData) {
const frame = args.object as frameModule.Frame;
frame.translateY = screen.mainScreen.heightDIPs;
frameModule.topmost().on('showBottomSheet', () => {
frame.animate({
duration: 1000,
translate: { x: 0, y: 200 },
curve: new CubicBezierAnimationCurve(.44, .63, 0, 1)
});
});
}
In the above code, we get the root/topmost frame and add a listener to it with the on()
function. Frames are Views and thus are Observables which is why we can call that method on the frame.
We register a custom event named showBottomSheet
that animates the frame upwards. We use CubicBezierAnimationCurve to define a custom animation timing curve by using the cubic-bezier function. Possible values are numeric values from 0 to 1. To test out different animations, you can use this cubic-bezier tool.
To trigger that event, let's implement the Show button's btnShowTap
event handler.
// home-page.ts
import { EventData } from "tns-core-modules/ui/page/page";
import { Button } from "tns-core-modules/ui/button";
import * as frameModule from "tns-core-modules/ui/frame";
export function page1BtnTap(args: EventData) {
const button = args.object as Button;
button.page.frame.navigate('page2-page');
}
export function btnShowTap(args: EventData) {
// Show botom sheet
frameModule.topmost().notify({ eventName: 'showBottomSheet', object: args.object });
}
When the Show button is tapped, we emit/trigger the showBottomSheet
custom event we had registered. The notify()
method takes any implementer of the EventData interface as event data. It includes basic information about an event—its name as eventName
and an instance of the event sender as object
. The eventName
is used to execute all event handlers associated with this event.
Run the app and you should be able to bring up the bottom sheet by tapping the Show button.
We can bring up the bottom sheet, but we have no way of dismissing it at the moment. We'll wire up the Tap Me button so that when it's tapped, the bottom sheet will animate back down below the screen. You can use a swipe gesture for this instead of a button tap if you prefer.
// action-page.ts
import * as frameModule from "tns-core-modules/ui//frame";
import { EventData } from "tns-core-modules/ui//frame";
export function tapMeTap(args: EventData) {
frameModule.topmost.()notify({ eventName: 'hideBottomSheet', object: args.object })
}
When the button is tapped, it will trigger the hideBottomSheet
event. Let's register that event:
// main-page.ts
import { EventData } from "tns-core-modules/data/observable";
import { Page } from "tns-core-modules/ui/page";
import { HelloWorldModel } from "./main-view-model";
import { screen, isIOS } from "tns-core-modules/platform";
import { CubicBezierAnimationCurve } from "tns-core-modules/ui/animation/animation";
import * as frameModule from "tns-core-modules/ui//frame";
export function actionFrameLoaded(args: EventData) {
const frame = args.object as frameModule.Frame;
frame.translateY = screen.mainScreen.heightDIPs;
frameModule.topmost().on('showBottomSheet', () => {
frame.animate({
duration: 1000,
translate: { x: 0, y: 200 },
curve: new CubicBezierAnimationCurve(.44, .63, 0, 1)
});
});
frameModule.topmost().on('hideBottomSheet', () => {
frame.animate({
duration: 1000,
translate: { x: 0, y: screen.mainScreen.heightDIPs },
curve: new CubicBezierAnimationCurve(.44, .63, 0, 1)
});
});
}
Now when the Tap Me button is tapped, the bottom sheet moves back down.
We can animate the bottom sheet onto the screen and dismiss it with the touch of a button. This is great, but there are ways we can improve the app. For instance, on some iOS apps, when a bottom sheet slides up a page, the page in the background shrinks a bit. Let's create this effect.
Since we are going to be scaling the top frame, we need to first add a loaded
event to it.
<!-- main-page.xml -->
<Page xmlns="http://schemas.nativescript.org/tns.xsd" actionBarHidden="true">
<GridLayout>
<Frame defaultPage="home-page" loaded="pageFrameLoaded">
</Frame>
<Frame defaultPage="action-page" loaded="actionFrameLoaded">
</Frame>
</GridLayout>
</Page>
Now let's handle the pageFrameLoaded
event handler:
// main-page.ts
import { EventData } from "tns-core-modules/data/observable";
import { Page } from "tns-core-modules/ui/page";
import { HelloWorldModel } from "./main-view-model";
import { screen, isIOS } from "tns-core-modules/platform";
import { CubicBezierAnimationCurve } from "tns-core-modules/ui/animation/animation";
import * as frameModule from "tns-core-modules/ui//frame";
export function pageFrameLoaded(args: EventData) {
const frame = args.object as frameModule.Frame;
frameModule.topmost().on('showBottomSheet', () => {
frame.borderRadius = 10;
frame.animate({
duration: 1000,
scale: { x: .95, y: .95 },
opacity: .6,
curve: new CubicBezierAnimationCurve(.44, .63, 0, 1)
});
});
frameModule.topmost().on('hideBottomSheet', () => {
frame.animate({
duration: 1000,
scale: { x: 1, y: 1 },
opacity: 1,
curve: new CubicBezierAnimationCurve(.44, .63, 0, 1)
}).then(() => { frame.borderRadius = 0; });
});
}
export function actionFrameLoaded(args: EventData) {
const frame = args.object as frameModule.Frame;
frame.translateY = screen.mainScreen.heightDIPs;
frameModule.topmost().on('showBottomSheet', () => {
frame.animate({
duration: 1000,
translate: { x: 0, y: 200 },
curve: new CubicBezierAnimationCurve(.44, .63, 0, 1)
});
});
frameModule.topmost().on('hideBottomSheet', () => {
frame.animate({
duration: 1000,
translate: { x: 0, y: screen.mainScreen.heightDIPs },
curve: new CubicBezierAnimationCurve(.44, .63, 0, 1)
});
});
}
In the above, we register the showBottomSheet
and hideBottomSheet
events which show and hide the bottom sheet respectively while scaling down the background page in case of the former and scaling the page back up in case of the latter.
Last thing to do, let's make the entire background of the page black so that it will look like the top frame is actually fading into darkness when we bring up the bottom sheet.
<!-- main-page.xml -->
<Page xmlns="http://schemas.nativescript.org/tns.xsd" actionBarHidden="true" backgroundColor="black">
...
</Page>
Run the app and the background frame should now darken and scale down when the bottom sheet slides onto the screen.
And that, folks, is one of the ways you can create an animated bottom sheet. We hoped you learned a thing or two!
Let me know what you thought of this tutorial on Twitter: @digitalix or leave a comment down below. You can also send me your NativeScript related questions that I can answer in video form. If I select your question to make a video answer, I'll send you swag. Use the #iScriptNative hashtag.
For more tutorials on NativeScript, check out our courses on NativeScripting.com. We have a NativeScript Hands-On UI course that covers NativeScript user interface, views and components. You might also be interested in the following two courses on styling NativeScript applications: Styling NativeScript Core Applications and Styling NativeScript with Angular Applications.