This is a quick tutorial on how to design a login screen with a background video file, which I recently added to my app WaveSpy (iOS, Android).
This is what the screen will look like:
I’m assuming you have a flutter dev env set up. If not, you can use Flutter’s Getting Started.
Now let’s create a new project named flutter_login_video
. In the terminal run:
flutter create --org com.learningsomethingnew.fluttervideo --project-name flutter_login_video flutter_login_video
It’s a good idea to do your first commit at this point
We’ll use the video_player plugin to display the video in the background.
Add the following line to pubspec.yaml
dependencies:
video_player: ^0.10.9+1
I’ve gotten good results with a 20 second 720p video - about 2 MB in size. So create such a video with your favorite video editor. Create a folder named assets
in the main project folder, and put your mp4
file in a subfolder named videos
(so it will be in assets/videos
eventually).
For your app’s logo, create a square png
with white foreground and transparent background. Put the image in assets/images
.
Now add the assets to pubspec.yaml
so flutter includes them in the generated app:
assets:
- assets/videos/
- assets/images/
We’ll create our new widget in sign_in.dart
. Let’s look at some initializations before building the UI:
We’ll initialize our video player in main.dart
’s initState
method:
_controller = VideoPlayerController.asset("assets/videos/waves.mp4");
_controller.initialize().then((_) {
_controller.setLooping(true);
Timer(Duration(milliseconds: 100), () {
setState(() {
_controller.play();
_visible = true;
});
});
});
We’re initializing the controller with the asset
method to load a local video. We’re setting the player to loops the video.
Notice the video will start playing after 100 milliseconds, which is a small optimization to make app load faster.
We need to also dispose of the controller in the dispose
override:
void dispose() {
super.dispose();
if (_controller != null) {
_controller.dispose();
_controller = null;
}
}
Forcing portrait or landscape mode should be used sparingly, but in this case I think it makes sense.
In my opinion there’s no use-case where users really have to use landscape mode for logging in. This is a UI flow that’s usually done in portrait mode, and it helps if we know that we’re going to display the video in a portrait aspect ratio.
We’ll use SystemChrome.setPreferredOrientations
to lock the device into portrait mode. In initState
add:
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
When you navigate to other screens that should support landscape (the rest of your app), just set:
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeRight,
DeviceOrientation.landscapeLeft,
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
Our UI will consist of a main centered Stack
, which overlays three layers:
Container
on top to give it a blue tint. Let’s get into the code:
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Stack(
children: <Widget>[
_getVideoBackground(),
_getBackgroundColor(),
_getContent(),
],
),
),
);
}
I’ve split the main UI components into methods to make the code more readable. Let’s look into each one of the UI building methods.
This is basically the VideoPlayer
widget (with the controller we initialized in initState
), wrapped with an AnimatedOpacity
widget to fade the video in.
_getVideoBackground() {
return AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: Duration(milliseconds: 1000),
child: VideoPlayer(_controller),
);
}
This is a simple Container
with a color that has less than 255 alpha.
_getBackgroundColor() {
return Container(
color: Colors.blue.withAlpha(120),
);
}
Now for the main content, which consists of the logo, title, subtitle, and buttons, all residing in one Column
:
_getContent() {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
SizedBox(
height: 50.0,
),
Image(
image: AssetImage("assets/images/white-logo.png"),
width: 150.0,
),
Text(
"WaveSpy",
style: TextStyle(color: Colors.white, fontSize: 40),
),
Container(
margin: const EdgeInsets.only(left: 30.0, right: 30.0, top: 40.0),
alignment: Alignment.center,
child: Text(
"View and share videos of current ocean conditions.",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
Spacer(),
..._getLoginButtons()
],
);
}
Notice the use of
Spacer
to make the buttons stick to the bottom of theColumn
. Notice also the use of the spread operator (added in sdk 2.2.2), that allows us to add the login buttons to thechildren
list ofColumn
in a functional manner.
Finally we’ll create the bottom login buttons:
_getLoginButtons() {
return <Widget>[
Container(
margin: const EdgeInsets.only(left: 20, right: 20),
width: double.infinity,
child: FlatButton(
color: Colors.white,
padding: const EdgeInsets.only(top: 15.0, bottom: 15.0),
child: const Text('Sign Up with Email'),
onPressed: () async {},
),
),
Container(
margin: const EdgeInsets.only(top: 10, left: 20, right: 20),
width: double.infinity,
child: FlatButton(
color: Colors.blueAccent,
padding: const EdgeInsets.only(top: 15.0, bottom: 15.0),
child: const Text(
'Log back in',
style: TextStyle(color: Colors.white),
),
onPressed: () async {},
),
),
Container(
margin: const EdgeInsets.only(bottom: 16, top: 20, left: 20, right: 20),
width: double.infinity,
child: FlatButton(
child: const Text(
'Later...',
style: TextStyle(color: Colors.white),
),
onPressed: () {},
),
),
];
}
That’s it! We have our login screen:
As always, full source can be found on GitHub. If you have any questions, please leave a comment!