Snapchat-like UI using Flutter.
A proof of concept on how to get the Snapchat-style swiping functionality using flutter.
This project includes:
- a working camera
- 'play/pause' button, instead of 'take picture'
- the 'color shading' animation that happens when a user swipes
- 'flip camera' button
- lock screen orientation
Here is the demo GIF:
Step 1: Create Flutter Project
- Open your favorite Flutter IDE.
- Go to
File-->New-->Project
to create a new project.
Step 2: Dependencies
We only use minimal dependencies:
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^0.1.0
camera: ^0.1.2
Step 3: Create Shade
Create a Shade as a statelesswidget:
import 'package:flutter/material.dart';
class Shade extends StatelessWidget {
final double opacity;
final bool isLeft;
Shade({this.opacity, this.isLeft});
@override
Widget build(BuildContext context) {
return new Positioned.fill(
child: new Opacity(
opacity: opacity,
child: new Container(
color: isLeft ? Colors.lightBlue : Colors.purple,
),
));
}
}
Step 4: Create Pager class
Create Pager class with a Left widget, Right widget and PageController
class:
import 'package:flutter/material.dart';
class Pager extends StatelessWidget {
final PageController controller;
final Widget leftWidget;
final Widget rightWidget;
Pager({this.controller, this.leftWidget, this.rightWidget});
Iterable<Widget> buildPages() {
final List<Widget> pages = <Widget>[];
pages.add(_contentWidget(Colors.white, leftWidget));
pages.add(_contentWidget(Colors.white));
pages.add(_contentWidget(Colors.white, rightWidget));
return pages;
}
_contentWidget(Color color, [Widget page]) {
var widgets = <Widget>[];
widgets.add(new Opacity(
opacity: 0.0,
child: new Container(
color: color,
),
));
if (page != null) {
widgets.add(new Positioned.fill(
child: new Container(
margin: new EdgeInsets.fromLTRB(0.0, 80.0, 0.0, 0.0),
child: page,
decoration: new ShapeDecoration(
color: Colors.white,
shape: new RoundedRectangleBorder(
borderRadius: new BorderRadius.only(
topLeft: new Radius.circular(10.0),
topRight: new Radius.circular(10.0)
)
)
),
),
));
}
return new Stack(
children: widgets,
);
}
@override
Widget build(BuildContext context) {
return new Positioned.fill(
child: new Directionality(
textDirection: TextDirection.ltr,
child: new PageView(
controller: controller,
children: buildPages(),
),
)
);
}
}
Step 5: Create Camera Page
Create our Camera page:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:camera/camera.dart';
List<CameraDescription> _cameras;
CameraController _controller;
IconData _cameraLensIcon(CameraLensDirection currentDirection) {
switch (currentDirection) {
case CameraLensDirection.back:
return Icons.camera_front;
case CameraLensDirection.front:
return Icons.camera_rear;
case CameraLensDirection.external:
return Icons.camera;
}
throw new ArgumentError('Unknown lens direction');
}
void playPause() {
if (_controller != null) {
if (_controller.value.isStarted) {
_controller.stop();
} else {
_controller.start();
}
}
}
Future<Null> _restartCamera(CameraDescription description) async {
final CameraController tempController = _controller;
_controller = null;
await tempController?.dispose();
_controller = new CameraController(description, ResolutionPreset.high);
await _controller.initialize();
}
Future<Null> flipCamera() async {
if (_controller != null) {
var newDescription = _cameras.firstWhere((desc) {
return desc.lensDirection != _controller.description.lensDirection;
});
await _restartCamera(newDescription);
}
}
class CameraIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (_controller != null) {
return new Icon(
_cameraLensIcon(_controller.description.lensDirection),
color: Colors.white,
);
} else {
return new Container();
}
}
}
class CameraHome extends StatefulWidget {
@override
_CameraHomeState createState() => new _CameraHomeState();
}
class _CameraHomeState extends State<CameraHome> with WidgetsBindingObserver {
bool opening = false;
String imagePath;
int pictureCount = 0;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
availableCameras().then((cams) {
_cameras = cams;
_controller = new CameraController(_cameras[1], ResolutionPreset.high);
_controller.initialize()
.then((_) {
if (!mounted) {
return;
}
setState(() {});
});
});
}
@override
void dispose() {
_controller?.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
var description = _cameras.firstWhere((desc) {
return desc.lensDirection == _controller.description.lensDirection;
});
_restartCamera(description)
.then((_) {
setState(() {});
});
}
}
@override
Widget build(BuildContext context) {
var screenDimension = MediaQuery.of(context).size;
final List<Widget> columnChildren = <Widget>[];
if (_controller != null && _controller.value.initialized) {
columnChildren.add(new Expanded(
child: new FittedBox(
fit: BoxFit.fitHeight,
alignment: AlignmentDirectional.center,
child: new Container(
width: screenDimension.width,
height: screenDimension.height * _controller.value.aspectRatio,
child: new CameraPreview(_controller),
)
)
));
} else {
columnChildren.add(new Center(
child: new Directionality(
textDirection: TextDirection.ltr,
child: new Icon(Icons.question_answer)
)
));
}
return new Column(
children: columnChildren,
);
}
Widget playPauseButton() {
return new Padding(
padding: const EdgeInsets.only(top: 25.0),
child: new FlatButton(
onPressed: () {
setState(() {
if (_controller.value.isStarted) {
_controller.stop();
} else {
_controller.start();
}
});
},
child: new Icon(
_controller.value.isStarted ? Icons.pause : Icons.play_arrow
),
),
);
}
}
Step 6: Create Circle Button
Create a custom circle button widget as shown below:
import 'dart:math';
import 'package:flutter/material.dart';
import 'dart:ui' show lerpDouble;
import 'camera_page.dart';
class ControlsLayer extends StatelessWidget {
final double offset;
final Function onTap;
final _ShadowTween shadowTween;
final _TakePictureTween buttonTween;
final CameraIcon cameraIcon;
final Function onCameraTap;
ControlsLayer({this.offset, this.onTap, this.cameraIcon, this.onCameraTap}) :
this.buttonTween = new _TakePictureTween(
new _TakePicture( 70.0, 100.0, onTap: onTap,),
new _TakePicture( 50.0, 80.0,)
),
this.shadowTween = new _ShadowTween(new _Shadow(-290.0), new _Shadow(-150.0));
@override
Widget build(BuildContext context) {
return new Stack(
children: <Widget>[
shadowTween.lerp(offset),
buttonTween.lerp(offset),
new _Controls(cameraIcon, onCameraTap)
],
);
}
}
class _Controls extends StatelessWidget {
final CameraIcon cameraIcon;
final Function onCameraTap;
_Controls(this.cameraIcon, this.onCameraTap);
@override
Widget build(BuildContext context) {
return new Positioned(
top: 35.0,
left: 20.0,
child: new SizedBox(
width: 20.0,
height: 40.0,
child: new GestureDetector(
onTap: onCameraTap,
child: cameraIcon,
),
),
);
}
}
class _TakePicture extends StatelessWidget {
final double bottom;
final double diameter;
final Function onTap;
_TakePicture(this.bottom, this.diameter, {this.onTap});
@override
Widget build(BuildContext context) {
return new Positioned(
bottom: bottom,
left: MediaQuery.of(context).size.width / 2 - 50,
child: new SizedBox(
width: 100.0,
height: 100.0,
child: new Container(
alignment: Alignment.bottomCenter,
child: new SizedBox(
width: diameter,
height: diameter,
child: new GestureDetector(
onTap: onTap,
child: new Container(
decoration: new ShapeDecoration(
shape: new CircleBorder(
side: new BorderSide(
width: 5.0, color: Colors.white
)
)
),
),
),
),
)
)
);
}
static _TakePicture lerp(_TakePicture begin, _TakePicture end, double t) {
return new _TakePicture(
lerpDouble(begin.bottom, end.bottom, t),
lerpDouble(begin.diameter, end.diameter, t),
onTap: begin.onTap
);
}
}
class _TakePictureTween extends Tween<_TakePicture> {
_TakePictureTween(_TakePicture begin, _TakePicture end)
: super(begin: begin, end: end);
_TakePicture lerp(double t) => _TakePicture.lerp(begin, end, t);
}
class _Shadow extends StatelessWidget {
final double bottom;
_Shadow(this.bottom);
final double shadowSize = 250.0;
@override
Widget build(BuildContext context) {
return new Positioned(
bottom: bottom,
left: MediaQuery.of(context).size.width / 2 - (shadowSize / 2),
child: new Transform.rotate(
angle: PI / 4,
child: new Container(
width: shadowSize,
height: shadowSize,
decoration: new BoxDecoration(boxShadow: <BoxShadow>[
new BoxShadow(color: Colors.black12, blurRadius: 20.0)
], borderRadius: new BorderRadius.all(new Radius.circular(40.0))),
),
),
);
}
static _Shadow lerp(_Shadow begin, _Shadow end, double t) {
return new _Shadow(lerpDouble(begin.bottom, end.bottom, t));
}
}
class _ShadowTween extends Tween<_Shadow> {
_ShadowTween(_Shadow begin, _Shadow end) : super(begin: begin, end: end);
@override
_Shadow lerp(double t) => _Shadow.lerp(begin, end, t);
}
Step 7: Create Main class
Create our main.dart
class as shown below:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'dart:ui';
import 'camera_page.dart';
import 'pager.dart';
import 'circle_button.dart';
import 'shade.dart';
class EntryPoint extends StatefulWidget {
_EntryPointState createState() => new _EntryPointState();
}
class _EntryPointState extends State<EntryPoint> {
final PageController pagerController =
new PageController(keepPage: true, initialPage: 1);
double buttonDiameter = 100.0;
double offsetRatio = 0.0;
double offsetFromOne = 0.0;
bool onPageView(ScrollNotification notification) {
if (notification.depth == 0 && notification is ScrollUpdateNotification) {
setState(() {
offsetFromOne = 1.0 - pagerController.page;
offsetRatio = offsetFromOne.abs();
});
}
return false;
}
@override
Widget build(BuildContext context) {
return new MediaQuery(
data: new MediaQueryData.fromWindow(window),
child: new Directionality(
textDirection: TextDirection.ltr,
child: new Scaffold(
body: new Stack(
children: <Widget>[
new CameraHome(),
new Shade(
opacity: offsetRatio,
isLeft: offsetFromOne > 0,
),
new NotificationListener<ScrollNotification>(
onNotification: onPageView,
child: new Pager(
controller: pagerController,
leftWidget: new ItemList(
amount: 30,
),
rightWidget: new Text('right'),
)
),
new ControlsLayer(
offset: offsetRatio,
onTap: () {
playPause();
},
cameraIcon: new CameraIcon(),
onCameraTap: () async {
await flipCamera();
setState(() {});
},
)
],
),
)
)
);
}
}
class ItemList extends StatelessWidget {
final int amount;
ItemList({this.amount});
@override
Widget build(BuildContext context) {
var children = <Widget>[];
for (int i = 0; i < amount; i++) {
children.add(new ListTile(
title: new Text('tile $i'),
));
}
return new ListView(
children: children,
);
}
}
void main() {
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
.then((_) {
runApp(new EntryPoint());
});
}
Run
To run the project:
flutter packages get
flutter run