WhatsApp Clone with Flutter in a week (Part 2)
Camera and AppBar animation
This development journey is shared through 2 articles. This is part two. Source code is on Github, branch master. You may find article Part 1 here.
Last time we ended at finishing the ChatRoom( ) with customisation of some widgets. In this article, we will add camera to our WhatsApp clone app, together with animation of the top app bar when user switches tabs.
Adding Camera
The Flutter team is actively developing different plugins, one of them being the camera. We will use it for sure. We can install a plugin by add entry to the pubspec.yaml file. We add the following details under the file’s dependencies section: camera: ^0.4.2
. We then make a few adjustment to the files in Android and IOS side respectively, according to the documentation.
What I did then was to read through their camera example which illustrates quite a bunch of functionalities such as taking picture, recording video, switching cameras, etc. I just picked what we need for our app. There are 5 steps:
- Find available cameras on the device at main.dart, before the app starts
- Initialise CameraController in a stateful widget’s initState method (i.e. when the widget is initially loaded)
- Start streaming through CameraPreview
- Take picture on button pressed, print the saved image path to console for debugging (_onTakePictureButtonPress)
- Place TabCamera( ) widget as the first tab view, showing on the left side of ChatList( ) in home.dart
- For reference you can find the code here.
Putting up the TabCamera( ) layout
Okay camera’s done, let’s complete the rest of the screen by adding the gallery bar and the control bar (flash, take picture, and switch camera). We want to put these widgets on top of the camera preview, Stack can be employed. We will put CameraPreivew( ) as the back layer and a Column as the front layer.
Regarding the Column inside the Stack, this Column expands its height to max (bordered thick and red), and stacks on top of the camera preview. Using MainAxisAlignment.end will “push” the gallery bar and control bar to the near-bottom of the screen which is what we want.
Let’s take a look at the _buildGalleryBar( ) method:
We are not returning a ListView( ). This is because the by default ListView will expand as much as it can in direction orthogonal (90º) to its scrolling direction. In this case it means our LitView will expand vertically without limit. As this gallery bar will be a child inside the final Stack layout which prefers its children to have size well-defined, we wrapped the ListView with a fixed-height Container.
Moving on to the _buildControlBar( ) method:
We put the “buttons” in a Row widget, and “space-around” them. For the take-picture button we want it to be a think-bordered white circle, so instead of using IconButton, we use Container with BoxDecoration to custom-make one. To make any widget “tappable” or perform any complex gesture detection such as drag / long-press / force-press, we wrap the widget with GestureDetector. We can then put the handler methods inside the respective onPressed / onTapped properties.
Switch cameras, show captured photo, access to photo gallery
I’ll briefly go through the remaining functional part of TabCamera( ) before we move on to the next layout challenge (animation of top app bar),
For switching camera, in the above mentioned 5-step camera setup, the first step is to find out all the available cameras on the device, and store the information in an array named cameras. The second step is to call the _initCamera( ) method which initialise cameraController with a selected element of the cameras array. Previously we picked index 0. Switching camera essentially means calling _initCamera( ) again but with a different index. So we will modify this method to intake an argument index, and also add a variable _cameraIndex to the TabCameraState to keep track of the camera we are using. You may find all these inside method _onSwitchCamera( ) here.
After user tapping “take picture” button, you may want to show the user the captured photo. The _takePicture( ) method will return the filePath for where the image is saved on device, we can then display it in another pushed route (again doing Navigator.push) using Image.file(File(filePath)).
You may wonder why the captured photos do not appear in the device’s photo gallery. This is because in the _takePicture( ) method we created the folder from the directory reference give by getApplicationDocumentsDirectory( ) in which files there will be private to our app only. To store the captured photo directly to device’s photo gallery like WhatsApp does, we can use getExternalStorageDirectory( ) instead. However, at the time of writing this method is available for Android only. If you want to check device OS and execute the above functions conditionally, you can use the Platform class.
Animating to and from the camera tab
Notice that in the real WhatsApp when you swipe from CHATS to camera, the top app bar move away from screen steadily. In this section we will explore the animation of top app bar. As we’ve already discussed in Part 1, to move the AppBar away from screen, using a negative-top-margin Container to wrap AppBar won’t work, so we’ll use Positioned and Stack (again).
We will put the AppBar and the TabView in a Stack, wrap AppBar by Positioned, keep updating the value of the Positioned’s “top” property, thereby gradually fading the AppBar away from screen as the user swipes the tab from CHATS to camera. Value of “top” can be calculated by applying a ratio: _tabController.animation.value will change from 1 to 0 when user swipes from CHATS to camera, and the “top” should at the same time change from 0 to -appBarHeight, so we can work out that ratio.
That seems smooth.. if user is swiping slowly. As a QA pro who checks an app with the thirst for perfection, changing tabs too fast will cause app to crash.
Browsing through the web I found a similar issue here, I applied a fix similar to the suggestions in the discussion thread to the plugin. As official patch has yet been released, I created a fork of the library and do the temporary fix myself. You can create your own fork or just use mine, by changing the pubspec.yaml file:
...dependencies:
flutter:
sdk: flutter
camera:
git:
url: git://github.com/ng-kode/plugins.git // your fork
path: packages/camera...
That look’s perfect !!! Ya… in Android.
In those notched iphones we will see crashed app bar whenever a AppBar is not used in the appBar property of a Scaffold. (See this issue) From the screen shot below you can see the the layout crash is caused by the unknown paddings applied to the top and bottom of the AppBar.
So make the AppBar layout ourselves. We use Column as the outermost layer, with the first child being a Row and the last child being the TabBar. Then inside the Row we have the title, and the 2 buttons on the right. To push the buttons to the right, we will wrap the title with Expanded.
Adding floating action buttons (FABs)
Next we will add floating action buttons to all the tab views (except the camera tab). In case you don’t know this Material Design’s term, floating action button (FAB) essentially means the bottom-right-handed button for which its position is “fixed”. With Flutter we can display a FAB by providing a widget to the floatingActionButton property of a Scaffold. Flutter provides a variety of styling options for the FAB such as center / float-end, circle / extended shape. For now we will stick with the real WhatsApp design which is a circle float-end FAB. As the functionality and icon of the FAB differ from tab to tab, we change these 2 properties according to the _tabController.index value. Index will be zero for the leftmost tab, incrementing on successive right-hand tabs.
Probably you’ve have noticed the problem. The change of FAB’s icon is delayed when the user swipe say from CHATS to STATUS (i.e. changed only after the sliding is completed).
To make the change more sensitive, say when user swipes from camera tab to CHATS, _tabController.animation.value will give us value from 0, 0.1, … to … 0.9, 1 continuously. Instead of waiting the value to be exactly 1, we will change the FAB icon earlier, say 0.7.
What else…
We haven’t handle the case when user presses on the FAB. Basically this would be call Navigator.push again, then re-using widgets TabCamera( ) and ChatRoom( ) as the next page whenever appropriate. We’ve handled pushing new route before.
Summary
In this article we’ve gone through
- Add camera to our app using flutter’s camera plugin
- Use Stack layout the customise the UI of TabCamera
- Top app bar fades away from screen top when user swipes to camera
- Self-make the app bar to fix the layout crash issue
- Check _tabController.animation.value to update FAB
And that’s it ! Hope you enjoy reading the article. Please let me know your thoughts / experience with Flutter 😉 Cheers !