SlideShare a Scribd company logo
Creating a WhatsApp Clone - Part V
Now that we implemented the model code and the lifecycle code we are almost finished. We are down to UI code and CSS.
public class MainForm extends Form {
private Tabs tabs = new Tabs();
private CameraKit ck;
private Container chats;
private Container status;
private Container calls;
private static MainForm instance;
public MainForm() {
super("WhatsApp Clone", new BorderLayout());
instance = this;
add(CENTER, tabs);
tabs.hideTabs();
ck = CameraKit.create();
tabs.addTab("", createCameraView());
tabs.addTab("", createChatsContainer());
tabs.addTab("", createStatusContainer());
tabs.addTab("", createCallsContainer());
MainForm
We start with the main form which covers the list of chat elements.

As is the case with all the forms in this app we derive from Form for simplicity.
public class MainForm extends Form {
private Tabs tabs = new Tabs();
private CameraKit ck;
private Container chats;
private Container status;
private Container calls;
private static MainForm instance;
public MainForm() {
super("WhatsApp Clone", new BorderLayout());
instance = this;
add(CENTER, tabs);
tabs.hideTabs();
ck = CameraKit.create();
tabs.addTab("", createCameraView());
tabs.addTab("", createChatsContainer());
tabs.addTab("", createStatusContainer());
tabs.addTab("", createCallsContainer());
MainForm
The main body of the form is a tabs component that allows us to switch between camera, status and calls
public class MainForm extends Form {
private Tabs tabs = new Tabs();
private CameraKit ck;
private Container chats;
private Container status;
private Container calls;
private static MainForm instance;
public MainForm() {
super("WhatsApp Clone", new BorderLayout());
instance = this;
add(CENTER, tabs);
tabs.hideTabs();
ck = CameraKit.create();
tabs.addTab("", createCameraView());
tabs.addTab("", createChatsContainer());
tabs.addTab("", createStatusContainer());
tabs.addTab("", createCallsContainer());
MainForm
Camera kit is used to implement the camera UI but due to a regression in the native library this code is currently commented out.
public class MainForm extends Form {
private Tabs tabs = new Tabs();
private CameraKit ck;
private Container chats;
private Container status;
private Container calls;
private static MainForm instance;
public MainForm() {
super("WhatsApp Clone", new BorderLayout());
instance = this;
add(CENTER, tabs);
tabs.hideTabs();
ck = CameraKit.create();
tabs.addTab("", createCameraView());
tabs.addTab("", createChatsContainer());
tabs.addTab("", createStatusContainer());
tabs.addTab("", createCallsContainer());
MainForm
These are the 3 tab containers, we make use of them in the scrolling logic later
public class MainForm extends Form {
private Tabs tabs = new Tabs();
private CameraKit ck;
private Container chats;
private Container status;
private Container calls;
private static MainForm instance;
public MainForm() {
super("WhatsApp Clone", new BorderLayout());
instance = this;
add(CENTER, tabs);
tabs.hideTabs();
ck = CameraKit.create();
tabs.addTab("", createCameraView());
tabs.addTab("", createChatsContainer());
tabs.addTab("", createStatusContainer());
tabs.addTab("", createCallsContainer());
MainForm
The main form is a singleton as we need a way to refresh it when we are in a different form
private Container status;
private Container calls;
private static MainForm instance;
public MainForm() {
super("WhatsApp Clone", new BorderLayout());
instance = this;
add(CENTER, tabs);
tabs.hideTabs();
ck = CameraKit.create();
tabs.addTab("", createCameraView());
tabs.addTab("", createChatsContainer());
tabs.addTab("", createStatusContainer());
tabs.addTab("", createCallsContainer());
tabs.setSelectedIndex(1);
Toolbar tb = getToolbar();
tb.setTitleComponent(createTitleComponent(chats, status, calls));
setBackCommand("", null, e -> {
if(tabs.getSelectedIndex() != 1) {
tabs.setSelectedIndex(1);
} else {
MainForm
The form itself uses a border layout to place the tabs in the center. We also save the form instance for later use
private Container status;
private Container calls;
private static MainForm instance;
public MainForm() {
super("WhatsApp Clone", new BorderLayout());
instance = this;
add(CENTER, tabs);
tabs.hideTabs();
ck = CameraKit.create();
tabs.addTab("", createCameraView());
tabs.addTab("", createChatsContainer());
tabs.addTab("", createStatusContainer());
tabs.addTab("", createCallsContainer());
tabs.setSelectedIndex(1);
Toolbar tb = getToolbar();
tb.setTitleComponent(createTitleComponent(chats, status, calls));
setBackCommand("", null, e -> {
if(tabs.getSelectedIndex() != 1) {
tabs.setSelectedIndex(1);
} else {
MainForm
We hide the tabs, it generally means that 

These aren't actually tabs, they are buttons we draw ourselves. The reason for that is the special animation we need in the title area
private Container status;
private Container calls;
private static MainForm instance;
public MainForm() {
super("WhatsApp Clone", new BorderLayout());
instance = this;
add(CENTER, tabs);
tabs.hideTabs();
ck = CameraKit.create();
tabs.addTab("", createCameraView());
tabs.addTab("", createChatsContainer());
tabs.addTab("", createStatusContainer());
tabs.addTab("", createCallsContainer());
tabs.setSelectedIndex(1);
Toolbar tb = getToolbar();
tb.setTitleComponent(createTitleComponent(chats, status, calls));
setBackCommand("", null, e -> {
if(tabs.getSelectedIndex() != 1) {
tabs.setSelectedIndex(1);
} else {
MainForm
We add the tabs and select the second one as the default as we don’t want to open with the camera view
tabs.addTab("", createCameraView());
tabs.addTab("", createChatsContainer());
tabs.addTab("", createStatusContainer());
tabs.addTab("", createCallsContainer());
tabs.setSelectedIndex(1);
Toolbar tb = getToolbar();
tb.setTitleComponent(createTitleComponent(chats, status, calls));
setBackCommand("", null, e -> {
if(tabs.getSelectedIndex() != 1) {
tabs.setSelectedIndex(1);
} else {
minimizeApplication();
}
});
}
public static MainForm getInstance() {
return instance;
}
private Container createCallsContainer() {
Container cnt = new Container(BoxLayout.y());
MainForm
Instead of using the title we use a title component which takes over the entire title area and lets us build whatever we want up there. I’ll discuss it more when covering
that method
tabs.addTab("", createCameraView());
tabs.addTab("", createChatsContainer());
tabs.addTab("", createStatusContainer());
tabs.addTab("", createCallsContainer());
tabs.setSelectedIndex(1);
Toolbar tb = getToolbar();
tb.setTitleComponent(createTitleComponent(chats, status, calls));
setBackCommand("", null, e -> {
if(tabs.getSelectedIndex() != 1) {
tabs.setSelectedIndex(1);
} else {
minimizeApplication();
}
});
}
public static MainForm getInstance() {
return instance;
}
private Container createCallsContainer() {
Container cnt = new Container(BoxLayout.y());
MainForm
The back command of the form respects the hardware back buttons in some devices and the Android native back arrow. Here we have custom behavior for the form. If
we are in a tab other than the first tab we need to return to that tab. Otherwise the app is minimized. This seems to be the behavior of the native app.
}
public static MainForm getInstance() {
return instance;
}
private Container createCallsContainer() {
Container cnt = new Container(BoxLayout.y());
calls = cnt;
cnt.setScrollableY(true);
MultiButton chat = new MultiButton("Person");
chat.setTextLine2("Date & time");
cnt.add(chat);
FloatingActionButton fab = FloatingActionButton.
createFAB(FontImage.MATERIAL_CALL);
return fab.bindFabToContainer(cnt);
}
private Container createStatusContainer() {
Container cnt = new Container(BoxLayout.y());
status = cnt;
cnt.setScrollableY(true);
MainForm
The calls container is a Y scrollable container.
}
public static MainForm getInstance() {
return instance;
}
private Container createCallsContainer() {
Container cnt = new Container(BoxLayout.y());
calls = cnt;
cnt.setScrollableY(true);
MultiButton chat = new MultiButton("Person");
chat.setTextLine2("Date & time");
cnt.add(chat);
FloatingActionButton fab = FloatingActionButton.
createFAB(FontImage.MATERIAL_CALL);
return fab.bindFabToContainer(cnt);
}
private Container createStatusContainer() {
Container cnt = new Container(BoxLayout.y());
status = cnt;
cnt.setScrollableY(true);
MainForm
This is simply a placeholder, I placed here a multibutton representing incoming/outgoing calls and a floating action button
FloatingActionButton fab = FloatingActionButton.
createFAB(FontImage.MATERIAL_CALL);
return fab.bindFabToContainer(cnt);
}
private Container createStatusContainer() {
Container cnt = new Container(BoxLayout.y());
status = cnt;
cnt.setScrollableY(true);
MultiButton chat = new MultiButton("My Status");
chat.setTextLine2("Tap to add status update");
cnt.add(chat);
FloatingActionButton fab = FloatingActionButton.
createFAB(FontImage.MATERIAL_CAMERA_ALT);
return fab.bindFabToContainer(cnt);
}
public void refreshChatsContainer() {
Server.fetchChatList(contacts -> {
chats.removeAll();
for(ChatContact c : contacts) {
MainForm
The same is true for the status container. This isn't an important part of the functionality with this tutorial
return fab.bindFabToContainer(cnt);
}
public void refreshChatsContainer() {
Server.fetchChatList(contacts -> {
chats.removeAll();
for(ChatContact c : contacts) {
MultiButton chat = new MultiButton(c.name.get());
chat.setTextLine2(c.tagline.get());
if(chat.getTextLine2() == null ||
chat.getTextLine2().length() == 0) {
chat.setTextLine2("...");
}
chat.setIcon(c.getLargeIcon());
chats.add(chat);
chat.addActionListener(e -> new ChatForm(c, this).show());
}
chats.revalidate();
});
}
private Container createChatsContainer() {
MainForm
You might recall that we invoke this method from the main UI to refresh the ongoing chat status.
return fab.bindFabToContainer(cnt);
}
public void refreshChatsContainer() {
Server.fetchChatList(contacts -> {
chats.removeAll();
for(ChatContact c : contacts) {
MultiButton chat = new MultiButton(c.name.get());
chat.setTextLine2(c.tagline.get());
if(chat.getTextLine2() == null ||
chat.getTextLine2().length() == 0) {
chat.setTextLine2("...");
}
chat.setIcon(c.getLargeIcon());
chats.add(chat);
chat.addActionListener(e -> new ChatForm(c, this).show());
}
chats.revalidate();
});
}
private Container createChatsContainer() {
MainForm
We fetch up to date data from the storage. This is an asynchronous call that returns on the EDT so the rest of the code goes into the lambda.
return fab.bindFabToContainer(cnt);
}
public void refreshChatsContainer() {
Server.fetchChatList(contacts -> {
chats.removeAll();
for(ChatContact c : contacts) {
MultiButton chat = new MultiButton(c.name.get());
chat.setTextLine2(c.tagline.get());
if(chat.getTextLine2() == null ||
chat.getTextLine2().length() == 0) {
chat.setTextLine2("...");
}
chat.setIcon(c.getLargeIcon());
chats.add(chat);
chat.addActionListener(e -> new ChatForm(c, this).show());
}
chats.revalidate();
});
}
private Container createChatsContainer() {
MainForm
We remove the old content as we’ll just re-add it
return fab.bindFabToContainer(cnt);
}
public void refreshChatsContainer() {
Server.fetchChatList(contacts -> {
chats.removeAll();
for(ChatContact c : contacts) {
MultiButton chat = new MultiButton(c.name.get());
chat.setTextLine2(c.tagline.get());
if(chat.getTextLine2() == null ||
chat.getTextLine2().length() == 0) {
chat.setTextLine2("...");
}
chat.setIcon(c.getLargeIcon());
chats.add(chat);
chat.addActionListener(e -> new ChatForm(c, this).show());
}
chats.revalidate();
});
}
private Container createChatsContainer() {
MainForm
We loop over the contacts and for every new contact we create a chat multi-button with the given name
return fab.bindFabToContainer(cnt);
}
public void refreshChatsContainer() {
Server.fetchChatList(contacts -> {
chats.removeAll();
for(ChatContact c : contacts) {
MultiButton chat = new MultiButton(c.name.get());
chat.setTextLine2(c.tagline.get());
if(chat.getTextLine2() == null ||
chat.getTextLine2().length() == 0) {
chat.setTextLine2("...");
}
chat.setIcon(c.getLargeIcon());
chats.add(chat);
chat.addActionListener(e -> new ChatForm(c, this).show());
}
chats.revalidate();
});
}
private Container createChatsContainer() {
MainForm
If there’s a tagline defined we set that tagline. We also use the large icon for that person
return fab.bindFabToContainer(cnt);
}
public void refreshChatsContainer() {
Server.fetchChatList(contacts -> {
chats.removeAll();
for(ChatContact c : contacts) {
MultiButton chat = new MultiButton(c.name.get());
chat.setTextLine2(c.tagline.get());
if(chat.getTextLine2() == null ||
chat.getTextLine2().length() == 0) {
chat.setTextLine2("...");
}
chat.setIcon(c.getLargeIcon());
chats.add(chat);
chat.addActionListener(e -> new ChatForm(c, this).show());
}
chats.revalidate();
});
}
private Container createChatsContainer() {
MainForm
If the button is clicked we show the chat form for this user
chat.addActionListener(e -> new ChatForm(c, this).show());
}
chats.revalidate();
});
}
private Container createChatsContainer() {
chats = new Container(BoxLayout.y());
chats.setScrollableY(true);
refreshChatsContainer();
FloatingActionButton fab = FloatingActionButton.
createFAB(FontImage.MATERIAL_CHAT);
fab.addActionListener(e -> new NewMessageForm().show());
return fab.bindFabToContainer(chats);
}
private Container createCameraView() {
if(ck != null) {
Container cameraCnt = new Container(new LayeredLayout());
MainForm
The chats container is the same as the other containers we saw but it’s actually fully implemented
chat.addActionListener(e -> new ChatForm(c, this).show());
}
chats.revalidate();
});
}
private Container createChatsContainer() {
chats = new Container(BoxLayout.y());
chats.setScrollableY(true);
refreshChatsContainer();
FloatingActionButton fab = FloatingActionButton.
createFAB(FontImage.MATERIAL_CHAT);
fab.addActionListener(e -> new NewMessageForm().show());
return fab.bindFabToContainer(chats);
}
private Container createCameraView() {
if(ck != null) {
Container cameraCnt = new Container(new LayeredLayout());
MainForm
It invokes the refresh chats container method we previously saw in order to fill up the container
chat.addActionListener(e -> new ChatForm(c, this).show());
}
chats.revalidate();
});
}
private Container createChatsContainer() {
chats = new Container(BoxLayout.y());
chats.setScrollableY(true);
refreshChatsContainer();
FloatingActionButton fab = FloatingActionButton.
createFAB(FontImage.MATERIAL_CHAT);
fab.addActionListener(e -> new NewMessageForm().show());
return fab.bindFabToContainer(chats);
}
private Container createCameraView() {
if(ck != null) {
Container cameraCnt = new Container(new LayeredLayout());
MainForm
The floating action button here is actually implemented by showing the new message form
}
private Container createCameraView() {
if(ck != null) {
Container cameraCnt = new Container(new LayeredLayout());
tabs.addSelectionListener((oldSelected, newSelected) -> {
if(newSelected == 0) {
//ck.start();
//cameraCnt.add(ck.getView());
getToolbar().setHidden(true);
} else {
if(oldSelected == 0) {
//cameraCnt.removeAll();
//ck.stop();
getToolbar().setHidden(false);
}
}
});
return cameraCnt;
}
return BorderLayout.center(new Label("Camera Unsupported"));
}
MainForm
Camera support is currently commented out due to a regression in the native library. However, the concept is relatively simple. We use the tab selection listener to
activate the camera as we need it
}
return BorderLayout.center(new Label("Camera Unsupported"));
}
private void showOverflowMenu() {
Button newGroup = new Button("New group", "Command");
Button newBroadcast = new Button("New broadcast", "Command");
Button whatsappWeb = new Button("WhatsApp Web", "Command");
Button starred = new Button("Starred Messages", "Command");
Button settings = new Button("Settings", "Command");
Container cnt = BoxLayout.encloseY(newGroup, newBroadcast,
whatsappWeb, starred, settings);
cnt.setUIID("CommandList");
Dialog dlg = new Dialog(new BorderLayout());
dlg.setDialogUIID("Container");
dlg.add(CENTER, cnt);
dlg.setDisposeWhenPointerOutOfBounds(true);
dlg.setTransitionInAnimator(CommonTransitions.createEmpty());
dlg.setTransitionOutAnimator(CommonTransitions.createEmpty());
dlg.setBackCommand("", null, e -> dlg.dispose());
int top = getUIManager().getComponentStyle("StatusBar").
getVerticalPadding();
setTintColor(0);
int bottom = getHeight() - cnt.getPreferredH() - top -
MainForm
The overflow menu is normally implemented in the toolbar but since I wanted more control over the toolbar area I chose to implement i manually in the code.
}
return BorderLayout.center(new Label("Camera Unsupported"));
}
private void showOverflowMenu() {
Button newGroup = new Button("New group", "Command");
Button newBroadcast = new Button("New broadcast", "Command");
Button whatsappWeb = new Button("WhatsApp Web", "Command");
Button starred = new Button("Starred Messages", "Command");
Button settings = new Button("Settings", "Command");
Container cnt = BoxLayout.encloseY(newGroup, newBroadcast,
whatsappWeb, starred, settings);
cnt.setUIID("CommandList");
Dialog dlg = new Dialog(new BorderLayout());
dlg.setDialogUIID("Container");
dlg.add(CENTER, cnt);
dlg.setDisposeWhenPointerOutOfBounds(true);
dlg.setTransitionInAnimator(CommonTransitions.createEmpty());
dlg.setTransitionOutAnimator(CommonTransitions.createEmpty());
dlg.setBackCommand("", null, e -> dlg.dispose());
int top = getUIManager().getComponentStyle("StatusBar").
getVerticalPadding();
setTintColor(0);
int bottom = getHeight() - cnt.getPreferredH() - top -
MainForm
I used buttons with the Command UIID and a Container with the CommandList UIID to create this UI. I used buttons with the Command UIID and a Container with the
CommandList UIID to create this UI. I’ll discuss the CSS that created this in the next lesson.
Button whatsappWeb = new Button("WhatsApp Web", "Command");
Button starred = new Button("Starred Messages", "Command");
Button settings = new Button("Settings", "Command");
Container cnt = BoxLayout.encloseY(newGroup, newBroadcast,
whatsappWeb, starred, settings);
cnt.setUIID("CommandList");
Dialog dlg = new Dialog(new BorderLayout());
dlg.setDialogUIID("Container");
dlg.add(CENTER, cnt);
dlg.setDisposeWhenPointerOutOfBounds(true);
dlg.setTransitionInAnimator(CommonTransitions.createEmpty());
dlg.setTransitionOutAnimator(CommonTransitions.createEmpty());
dlg.setBackCommand("", null, e -> dlg.dispose());
int top = getUIManager().getComponentStyle("StatusBar").
getVerticalPadding();
setTintColor(0);
int bottom = getHeight() - cnt.getPreferredH() - top -
cnt.getUnselectedStyle().getVerticalPadding() -
cnt.getUnselectedStyle().getVerticalMargins();
int w = getWidth();
int left = w - cnt.getPreferredW() -
cnt.getUnselectedStyle().getHorizontalPadding() -
cnt.getUnselectedStyle().getHorizontalMargins();
dlg.show(top, bottom, left, 0);
MainForm
I create a transparent dialog by giving it the Container UIID. I place the menu in the center
Button whatsappWeb = new Button("WhatsApp Web", "Command");
Button starred = new Button("Starred Messages", "Command");
Button settings = new Button("Settings", "Command");
Container cnt = BoxLayout.encloseY(newGroup, newBroadcast,
whatsappWeb, starred, settings);
cnt.setUIID("CommandList");
Dialog dlg = new Dialog(new BorderLayout());
dlg.setDialogUIID("Container");
dlg.add(CENTER, cnt);
dlg.setDisposeWhenPointerOutOfBounds(true);
dlg.setTransitionInAnimator(CommonTransitions.createEmpty());
dlg.setTransitionOutAnimator(CommonTransitions.createEmpty());
dlg.setBackCommand("", null, e -> dlg.dispose());
int top = getUIManager().getComponentStyle("StatusBar").
getVerticalPadding();
setTintColor(0);
int bottom = getHeight() - cnt.getPreferredH() - top -
cnt.getUnselectedStyle().getVerticalPadding() -
cnt.getUnselectedStyle().getVerticalMargins();
int w = getWidth();
int left = w - cnt.getPreferredW() -
cnt.getUnselectedStyle().getHorizontalPadding() -
cnt.getUnselectedStyle().getHorizontalMargins();
dlg.show(top, bottom, left, 0);
MainForm
The dialog has no transition and disposed if the user taps outside of it or uses the back button
whatsappWeb, starred, settings);
cnt.setUIID("CommandList");
Dialog dlg = new Dialog(new BorderLayout());
dlg.setDialogUIID("Container");
dlg.add(CENTER, cnt);
dlg.setDisposeWhenPointerOutOfBounds(true);
dlg.setTransitionInAnimator(CommonTransitions.createEmpty());
dlg.setTransitionOutAnimator(CommonTransitions.createEmpty());
dlg.setBackCommand("", null, e -> dlg.dispose());
int top = getUIManager().getComponentStyle("StatusBar").
getVerticalPadding();
setTintColor(0);
int bottom = getHeight() - cnt.getPreferredH() - top -
cnt.getUnselectedStyle().getVerticalPadding() -
cnt.getUnselectedStyle().getVerticalMargins();
int w = getWidth();
int left = w - cnt.getPreferredW() -
cnt.getUnselectedStyle().getHorizontalPadding() -
cnt.getUnselectedStyle().getHorizontalMargins();
dlg.show(top, bottom, left, 0);
}
private Container createTitleComponent(Container... scrollables) {
Label title = new Label("WhatsApp", "Title");
MainForm
This disables the default darkening of the form when a dialog is shown
whatsappWeb, starred, settings);
cnt.setUIID("CommandList");
Dialog dlg = new Dialog(new BorderLayout());
dlg.setDialogUIID("Container");
dlg.add(CENTER, cnt);
dlg.setDisposeWhenPointerOutOfBounds(true);
dlg.setTransitionInAnimator(CommonTransitions.createEmpty());
dlg.setTransitionOutAnimator(CommonTransitions.createEmpty());
dlg.setBackCommand("", null, e -> dlg.dispose());
int top = getUIManager().getComponentStyle("StatusBar").
getVerticalPadding();
setTintColor(0);
int bottom = getHeight() - cnt.getPreferredH() - top -
cnt.getUnselectedStyle().getVerticalPadding() -
cnt.getUnselectedStyle().getVerticalMargins();
int w = getWidth();
int left = w - cnt.getPreferredW() -
cnt.getUnselectedStyle().getHorizontalPadding() -
cnt.getUnselectedStyle().getHorizontalMargins();
dlg.show(top, bottom, left, 0);
}
private Container createTitleComponent(Container... scrollables) {
Label title = new Label("WhatsApp", "Title");
MainForm
This version of the show method places the dialog with a fixed distance from the edges. We give it a small margin on the top to take the status bar into account. Then use
left and bottom margin to push the dialog to the top right side.

This gives us a lot of flexibility and allows us to show the dialog in any way we want.
cnt.getUnselectedStyle().getVerticalPadding() -
cnt.getUnselectedStyle().getVerticalMargins();
int w = getWidth();
int left = w - cnt.getPreferredW() -
cnt.getUnselectedStyle().getHorizontalPadding() -
cnt.getUnselectedStyle().getHorizontalMargins();
dlg.show(top, bottom, left, 0);
}
private Container createTitleComponent(Container... scrollables) {
Label title = new Label("WhatsApp", "Title");
Container titleArea;
if(title.getUnselectedStyle().getAlignment() == LEFT) {
titleArea = BorderLayout.center(title);
} else {
// for iOS we want the title to center properly
titleArea = BorderLayout.centerAbsolute(title);
}
Button search = new Button("", FontImage.MATERIAL_SEARCH, "Title");
Button overflow = new Button("", FontImage.MATERIAL_MORE_VERT,
"Title");
overflow.addActionListener(e -> showOverflowMenu());
titleArea.add(EAST, GridLayout.encloseIn(2, search, overflow));
MainForm
This method creates the title component for the form

which is this region. The method accepts the scrollable containers in the tabs container. This allows us to track scrolling and seamlessly fold the title area
cnt.getUnselectedStyle().getVerticalPadding() -
cnt.getUnselectedStyle().getVerticalMargins();
int w = getWidth();
int left = w - cnt.getPreferredW() -
cnt.getUnselectedStyle().getHorizontalPadding() -
cnt.getUnselectedStyle().getHorizontalMargins();
dlg.show(top, bottom, left, 0);
}
private Container createTitleComponent(Container... scrollables) {
Label title = new Label("WhatsApp", "Title");
Container titleArea;
if(title.getUnselectedStyle().getAlignment() == LEFT) {
titleArea = BorderLayout.center(title);
} else {
// for iOS we want the title to center properly
titleArea = BorderLayout.centerAbsolute(title);
}
Button search = new Button("", FontImage.MATERIAL_SEARCH, "Title");
Button overflow = new Button("", FontImage.MATERIAL_MORE_VERT,
"Title");
overflow.addActionListener(e -> showOverflowMenu());
titleArea.add(EAST, GridLayout.encloseIn(2, search, overflow));
MainForm
The title itself is just a label with the “Title” UIID. It’s placed in the center of the title area border layout. If we are on iOS we want the title to be centered, in that case we
need to use the center version of the border layout. The reason for this is that center alignment doesn’t know about the full layout and would center based on available
space. It would ignore the search and overflow buttons on the right when centering since it isn’t aware of other components.

However, using the center alignment and placing these buttons in the east solves that problem and gives us the correct title position.
if(title.getUnselectedStyle().getAlignment() == LEFT) {
titleArea = BorderLayout.center(title);
} else {
// for iOS we want the title to center properly
titleArea = BorderLayout.centerAbsolute(title);
}
Button search = new Button("", FontImage.MATERIAL_SEARCH, "Title");
Button overflow = new Button("", FontImage.MATERIAL_MORE_VERT,
"Title");
overflow.addActionListener(e -> showOverflowMenu());
titleArea.add(EAST, GridLayout.encloseIn(2, search, overflow));
ButtonGroup bg = new ButtonGroup();
RadioButton camera = RadioButton.createToggle("", bg);
camera.setUIID("SubTitle");
FontImage.setMaterialIcon(camera, FontImage.MATERIAL_CAMERA_ALT);
RadioButton chats = RadioButton.createToggle("Chats", bg);
RadioButton status = RadioButton.createToggle("Status", bg);
RadioButton calls = RadioButton.createToggle("Calls", bg);
chats.setUIID("SubTitle");
status.setUIID("SubTitle");
calls.setUIID("SubTitle");
RadioButton[] buttons = new RadioButton[] {
MainForm
The search and overflow commands are just buttons with the “Title” UIID. We already discussed the showOverflowMenu() method so this should be pretty obvious. We
just place the two buttons in the grid. I chose not to use a Command as this might create a misalignment for this use case and wouldn’t have saved on the amount of
code I had to write.
Button search = new Button("", FontImage.MATERIAL_SEARCH, "Title");
Button overflow = new Button("", FontImage.MATERIAL_MORE_VERT,
"Title");
overflow.addActionListener(e -> showOverflowMenu());
titleArea.add(EAST, GridLayout.encloseIn(2, search, overflow));
ButtonGroup bg = new ButtonGroup();
RadioButton camera = RadioButton.createToggle("", bg);
camera.setUIID("SubTitle");
FontImage.setMaterialIcon(camera, FontImage.MATERIAL_CAMERA_ALT);
RadioButton chats = RadioButton.createToggle("Chats", bg);
RadioButton status = RadioButton.createToggle("Status", bg);
RadioButton calls = RadioButton.createToggle("Calls", bg);
chats.setUIID("SubTitle");
status.setUIID("SubTitle");
calls.setUIID("SubTitle");
RadioButton[] buttons = new RadioButton[] {
camera, chats, status, calls
};
TableLayout tb = new TableLayout(2, 4);
Container toggles = new Container(tb);
MainForm
These are the tabs for selecting camera, chat etc… They are just toggle buttons which in this case are classified as radio buttons. This means only one of the radio
buttons within the button group can be selected. We give them all the SubTitle UIID which again I’ll discuss in the next lesson.
chats.setUIID("SubTitle");
status.setUIID("SubTitle");
calls.setUIID("SubTitle");
RadioButton[] buttons = new RadioButton[] {
camera, chats, status, calls
};
TableLayout tb = new TableLayout(2, 4);
Container toggles = new Container(tb);
toggles.add(tb.createConstraint().widthPercentage(10), camera);
toggles.add(tb.createConstraint().widthPercentage(30), chats);
toggles.add(tb.createConstraint().widthPercentage(30), status);
toggles.add(tb.createConstraint().widthPercentage(30), calls);
Label whiteLine = new Label("", "SubTitleUnderline");
whiteLine.setShowEvenIfBlank(true);
toggles.add(tb.createConstraint(1, 1) ,whiteLine);
final Container finalTitle = titleArea;
for(int iter = 0 ; iter < buttons.length ; iter++) {
final int current = iter;
buttons[iter].addActionListener(e -> {
tabs.setSelectedIndex(current);
MainForm
We use table layout to place the tabs into the UI, this allows us to explicitly determine the width of the columns.

Notice that the table layout has 2 rows…
TableLayout tb = new TableLayout(2, 4);
Container toggles = new Container(tb);
toggles.add(tb.createConstraint().widthPercentage(10), camera);
toggles.add(tb.createConstraint().widthPercentage(30), chats);
toggles.add(tb.createConstraint().widthPercentage(30), status);
toggles.add(tb.createConstraint().widthPercentage(30), calls);
Label whiteLine = new Label("", "SubTitleUnderline");
whiteLine.setShowEvenIfBlank(true);
toggles.add(tb.createConstraint(1, 1) ,whiteLine);
final Container finalTitle = titleArea;
for(int iter = 0 ; iter < buttons.length ; iter++) {
final int current = iter;
buttons[iter].addActionListener(e -> {
tabs.setSelectedIndex(current);
whiteLine.remove();
toggles.add(tb.createConstraint(1, current) ,whiteLine);
finalTitle.setPreferredSize(null);
toggles.animateLayout(100);
});
}
MainForm
The second row of the table contains a white line using the “SideTitleUnderline” UIID. This line is placed in row one and column one so it’s under the chats entry. When
we move between the tabs this underline needs to animate to the new position.
Label whiteLine = new Label("", "SubTitleUnderline");
whiteLine.setShowEvenIfBlank(true);
toggles.add(tb.createConstraint(1, 1) ,whiteLine);
final Container finalTitle = titleArea;
for(int iter = 0 ; iter < buttons.length ; iter++) {
final int current = iter;
buttons[iter].addActionListener(e -> {
tabs.setSelectedIndex(current);
whiteLine.remove();
toggles.add(tb.createConstraint(1, current) ,whiteLine);
finalTitle.setPreferredSize(null);
toggles.animateLayout(100);
});
}
tabs.addSelectionListener((oldSelected, newSelected) -> {
if(!buttons[newSelected].isSelected()) {
finalTitle.setPreferredSize(null);
buttons[newSelected].setSelected(true);
whiteLine.remove();
toggles.add(tb.createConstraint(1, newSelected) ,whiteLine);
toggles.animateLayout(100);
MainForm
Here we bind listeners to all the four buttons mapping to each tab.
Label whiteLine = new Label("", "SubTitleUnderline");
whiteLine.setShowEvenIfBlank(true);
toggles.add(tb.createConstraint(1, 1) ,whiteLine);
final Container finalTitle = titleArea;
for(int iter = 0 ; iter < buttons.length ; iter++) {
final int current = iter;
buttons[iter].addActionListener(e -> {
tabs.setSelectedIndex(current);
whiteLine.remove();
toggles.add(tb.createConstraint(1, current) ,whiteLine);
finalTitle.setPreferredSize(null);
toggles.animateLayout(100);
});
}
tabs.addSelectionListener((oldSelected, newSelected) -> {
if(!buttons[newSelected].isSelected()) {
finalTitle.setPreferredSize(null);
buttons[newSelected].setSelected(true);
whiteLine.remove();
toggles.add(tb.createConstraint(1, newSelected) ,whiteLine);
toggles.animateLayout(100);
MainForm
When a button is clicked we select the appropriate tab
Label whiteLine = new Label("", "SubTitleUnderline");
whiteLine.setShowEvenIfBlank(true);
toggles.add(tb.createConstraint(1, 1) ,whiteLine);
final Container finalTitle = titleArea;
for(int iter = 0 ; iter < buttons.length ; iter++) {
final int current = iter;
buttons[iter].addActionListener(e -> {
tabs.setSelectedIndex(current);
whiteLine.remove();
toggles.add(tb.createConstraint(1, current) ,whiteLine);
finalTitle.setPreferredSize(null);
toggles.animateLayout(100);
});
}
tabs.addSelectionListener((oldSelected, newSelected) -> {
if(!buttons[newSelected].isSelected()) {
finalTitle.setPreferredSize(null);
buttons[newSelected].setSelected(true);
whiteLine.remove();
toggles.add(tb.createConstraint(1, newSelected) ,whiteLine);
toggles.animateLayout(100);
MainForm
The next two lines implement the underline animation effect that we see when we click a button. Notice how the line animates to the right tab button.

To achieve this we remove the current white line and add it back to the toggle container in the right position.
Label whiteLine = new Label("", "SubTitleUnderline");
whiteLine.setShowEvenIfBlank(true);
toggles.add(tb.createConstraint(1, 1) ,whiteLine);
final Container finalTitle = titleArea;
for(int iter = 0 ; iter < buttons.length ; iter++) {
final int current = iter;
buttons[iter].addActionListener(e -> {
tabs.setSelectedIndex(current);
whiteLine.remove();
toggles.add(tb.createConstraint(1, current) ,whiteLine);
finalTitle.setPreferredSize(null);
toggles.animateLayout(100);
});
}
tabs.addSelectionListener((oldSelected, newSelected) -> {
if(!buttons[newSelected].isSelected()) {
finalTitle.setPreferredSize(null);
buttons[newSelected].setSelected(true);
whiteLine.remove();
toggles.add(tb.createConstraint(1, newSelected) ,whiteLine);
toggles.animateLayout(100);
MainForm
We reset the height of the title in case it was shrunk during scrolling
Label whiteLine = new Label("", "SubTitleUnderline");
whiteLine.setShowEvenIfBlank(true);
toggles.add(tb.createConstraint(1, 1) ,whiteLine);
final Container finalTitle = titleArea;
for(int iter = 0 ; iter < buttons.length ; iter++) {
final int current = iter;
buttons[iter].addActionListener(e -> {
tabs.setSelectedIndex(current);
whiteLine.remove();
toggles.add(tb.createConstraint(1, current) ,whiteLine);
finalTitle.setPreferredSize(null);
toggles.animateLayout(100);
});
}
tabs.addSelectionListener((oldSelected, newSelected) -> {
if(!buttons[newSelected].isSelected()) {
finalTitle.setPreferredSize(null);
buttons[newSelected].setSelected(true);
whiteLine.remove();
toggles.add(tb.createConstraint(1, newSelected) ,whiteLine);
toggles.animateLayout(100);
MainForm
And we finally update the layout with an animation which performs the actual line move animation
whiteLine.remove();
toggles.add(tb.createConstraint(1, current) ,whiteLine);
finalTitle.setPreferredSize(null);
toggles.animateLayout(100);
});
}
tabs.addSelectionListener((oldSelected, newSelected) -> {
if(!buttons[newSelected].isSelected()) {
finalTitle.setPreferredSize(null);
buttons[newSelected].setSelected(true);
whiteLine.remove();
toggles.add(tb.createConstraint(1, newSelected) ,whiteLine);
toggles.animateLayout(100);
}
});
bindFolding(titleArea, titleArea.getPreferredH(), scrollables);
return BoxLayout.encloseY(titleArea, toggles);
}
private void bindFolding(Container titleArea, int titleHeight,
Container... scrollables) {
MainForm
The previous block updated the tab selection when we select a button. This block does the opposite. It updates the button selection when we swipe the tabs. It uses a
tab selection listener
whiteLine.remove();
toggles.add(tb.createConstraint(1, current) ,whiteLine);
finalTitle.setPreferredSize(null);
toggles.animateLayout(100);
});
}
tabs.addSelectionListener((oldSelected, newSelected) -> {
if(!buttons[newSelected].isSelected()) {
finalTitle.setPreferredSize(null);
buttons[newSelected].setSelected(true);
whiteLine.remove();
toggles.add(tb.createConstraint(1, newSelected) ,whiteLine);
toggles.animateLayout(100);
}
});
bindFolding(titleArea, titleArea.getPreferredH(), scrollables);
return BoxLayout.encloseY(titleArea, toggles);
}
private void bindFolding(Container titleArea, int titleHeight,
Container... scrollables) {
MainForm
If the button isn't selected then we need to update it
whiteLine.remove();
toggles.add(tb.createConstraint(1, current) ,whiteLine);
finalTitle.setPreferredSize(null);
toggles.animateLayout(100);
});
}
tabs.addSelectionListener((oldSelected, newSelected) -> {
if(!buttons[newSelected].isSelected()) {
finalTitle.setPreferredSize(null);
buttons[newSelected].setSelected(true);
whiteLine.remove();
toggles.add(tb.createConstraint(1, newSelected) ,whiteLine);
toggles.animateLayout(100);
}
});
bindFolding(titleArea, titleArea.getPreferredH(), scrollables);
return BoxLayout.encloseY(titleArea, toggles);
}
private void bindFolding(Container titleArea, int titleHeight,
Container... scrollables) {
MainForm
Again we need to reset the title size
whiteLine.remove();
toggles.add(tb.createConstraint(1, current) ,whiteLine);
finalTitle.setPreferredSize(null);
toggles.animateLayout(100);
});
}
tabs.addSelectionListener((oldSelected, newSelected) -> {
if(!buttons[newSelected].isSelected()) {
finalTitle.setPreferredSize(null);
buttons[newSelected].setSelected(true);
whiteLine.remove();
toggles.add(tb.createConstraint(1, newSelected) ,whiteLine);
toggles.animateLayout(100);
}
});
bindFolding(titleArea, titleArea.getPreferredH(), scrollables);
return BoxLayout.encloseY(titleArea, toggles);
}
private void bindFolding(Container titleArea, int titleHeight,
Container... scrollables) {
MainForm
Next we select the button that matches the tab
tabs.addSelectionListener((oldSelected, newSelected) -> {
if(!buttons[newSelected].isSelected()) {
finalTitle.setPreferredSize(null);
buttons[newSelected].setSelected(true);
whiteLine.remove();
toggles.add(tb.createConstraint(1, newSelected) ,whiteLine);
toggles.animateLayout(100);
}
});
bindFolding(titleArea, titleArea.getPreferredH(), scrollables);
return BoxLayout.encloseY(titleArea, toggles);
}
private void bindFolding(Container titleArea, int titleHeight,
Container... scrollables) {
addPointerReleasedListener(e -> {
if(titleArea.getHeight() != titleHeight &&
titleArea.getHeight() != 0) {
if(titleHeight - titleArea.getHeight() > titleHeight / 2) {
titleArea.setPreferredSize(null);
MainForm
Finally we perform the animation of moving the underline between the tabs. Notice that this is almost identical to the previous animation code only in this case it’s
triggered by a dragging of the tabs instead of the button click event.
tabs.addSelectionListener((oldSelected, newSelected) -> {
if(!buttons[newSelected].isSelected()) {
finalTitle.setPreferredSize(null);
buttons[newSelected].setSelected(true);
whiteLine.remove();
toggles.add(tb.createConstraint(1, newSelected) ,whiteLine);
toggles.animateLayout(100);
}
});
bindFolding(titleArea, titleArea.getPreferredH(), scrollables);
return BoxLayout.encloseY(titleArea, toggles);
}
private void bindFolding(Container titleArea, int titleHeight,
Container... scrollables) {
addPointerReleasedListener(e -> {
if(titleArea.getHeight() != titleHeight &&
titleArea.getHeight() != 0) {
if(titleHeight - titleArea.getHeight() > titleHeight / 2) {
titleArea.setPreferredSize(null);
MainForm
The last two lines in this method are the bindFolding call which we will discuss soon and the box layout Y which wraps the two containers as one.
bindFolding(titleArea, titleArea.getPreferredH(), scrollables);
return BoxLayout.encloseY(titleArea, toggles);
}
private void bindFolding(Container titleArea, int titleHeight,
Container... scrollables) {
addPointerReleasedListener(e -> {
if(titleArea.getHeight() != titleHeight &&
titleArea.getHeight() != 0) {
if(titleHeight - titleArea.getHeight() > titleHeight / 2) {
titleArea.setPreferredSize(null);
} else {
titleArea.setPreferredH(0);
}
titleArea.getParent().animateLayout(100);
}
});
for(Container c : scrollables) {
c.addScrollListener((scrollX, scrollY, oldscrollX,
oldscrollY) -> {
// special case for tensile drag
if(scrollY <= 10) {
titleArea.setPreferredSize(null);
MainForm
The bindFolding method implements this animation of folding title. It's implemented by tracking pointer drag events and shrinking the title.
bindFolding(titleArea, titleArea.getPreferredH(), scrollables);
return BoxLayout.encloseY(titleArea, toggles);
}
private void bindFolding(Container titleArea, int titleHeight,
Container... scrollables) {
addPointerReleasedListener(e -> {
if(titleArea.getHeight() != titleHeight &&
titleArea.getHeight() != 0) {
if(titleHeight - titleArea.getHeight() > titleHeight / 2) {
titleArea.setPreferredSize(null);
} else {
titleArea.setPreferredH(0);
}
titleArea.getParent().animateLayout(100);
}
});
for(Container c : scrollables) {
c.addScrollListener((scrollX, scrollY, oldscrollX,
oldscrollY) -> {
// special case for tensile drag
if(scrollY <= 10) {
titleArea.setPreferredSize(null);
MainForm
When the pointer is released we need to check if the title shrunk enough to minimize or not enough so it would go back to the full size.
bindFolding(titleArea, titleArea.getPreferredH(), scrollables);
return BoxLayout.encloseY(titleArea, toggles);
}
private void bindFolding(Container titleArea, int titleHeight,
Container... scrollables) {
addPointerReleasedListener(e -> {
if(titleArea.getHeight() != titleHeight &&
titleArea.getHeight() != 0) {
if(titleHeight - titleArea.getHeight() > titleHeight / 2) {
titleArea.setPreferredSize(null);
} else {
titleArea.setPreferredH(0);
}
titleArea.getParent().animateLayout(100);
}
});
for(Container c : scrollables) {
c.addScrollListener((scrollX, scrollY, oldscrollX,
oldscrollY) -> {
// special case for tensile drag
if(scrollY <= 10) {
titleArea.setPreferredSize(null);
MainForm
If the title area height is different from the original height it means we are in the process of shrinking the title.
bindFolding(titleArea, titleArea.getPreferredH(), scrollables);
return BoxLayout.encloseY(titleArea, toggles);
}
private void bindFolding(Container titleArea, int titleHeight,
Container... scrollables) {
addPointerReleasedListener(e -> {
if(titleArea.getHeight() != titleHeight &&
titleArea.getHeight() != 0) {
if(titleHeight - titleArea.getHeight() > titleHeight / 2) {
titleArea.setPreferredSize(null);
} else {
titleArea.setPreferredH(0);
}
titleArea.getParent().animateLayout(100);
}
});
for(Container c : scrollables) {
c.addScrollListener((scrollX, scrollY, oldscrollX,
oldscrollY) -> {
// special case for tensile drag
if(scrollY <= 10) {
titleArea.setPreferredSize(null);
MainForm
In that case we need to decide whether the process is closer to the finish line or to the start
bindFolding(titleArea, titleArea.getPreferredH(), scrollables);
return BoxLayout.encloseY(titleArea, toggles);
}
private void bindFolding(Container titleArea, int titleHeight,
Container... scrollables) {
addPointerReleasedListener(e -> {
if(titleArea.getHeight() != titleHeight &&
titleArea.getHeight() != 0) {
if(titleHeight - titleArea.getHeight() > titleHeight / 2) {
titleArea.setPreferredSize(null);
} else {
titleArea.setPreferredH(0);
}
titleArea.getParent().animateLayout(100);
}
});
for(Container c : scrollables) {
c.addScrollListener((scrollX, scrollY, oldscrollX,
oldscrollY) -> {
// special case for tensile drag
if(scrollY <= 10) {
titleArea.setPreferredSize(null);
MainForm
If it’s less than half way to the height of the title we reset the preferred size of the title area. That means the title area will take up it’s original preferred size and grow back
to full height
bindFolding(titleArea, titleArea.getPreferredH(), scrollables);
return BoxLayout.encloseY(titleArea, toggles);
}
private void bindFolding(Container titleArea, int titleHeight,
Container... scrollables) {
addPointerReleasedListener(e -> {
if(titleArea.getHeight() != titleHeight &&
titleArea.getHeight() != 0) {
if(titleHeight - titleArea.getHeight() > titleHeight / 2) {
titleArea.setPreferredSize(null);
} else {
titleArea.setPreferredH(0);
}
titleArea.getParent().animateLayout(100);
}
});
for(Container c : scrollables) {
c.addScrollListener((scrollX, scrollY, oldscrollX,
oldscrollY) -> {
// special case for tensile drag
if(scrollY <= 10) {
titleArea.setPreferredSize(null);
MainForm
Otherwise we set the title area height to 0 so it’s effectively hidden
bindFolding(titleArea, titleArea.getPreferredH(), scrollables);
return BoxLayout.encloseY(titleArea, toggles);
}
private void bindFolding(Container titleArea, int titleHeight,
Container... scrollables) {
addPointerReleasedListener(e -> {
if(titleArea.getHeight() != titleHeight &&
titleArea.getHeight() != 0) {
if(titleHeight - titleArea.getHeight() > titleHeight / 2) {
titleArea.setPreferredSize(null);
} else {
titleArea.setPreferredH(0);
}
titleArea.getParent().animateLayout(100);
}
});
for(Container c : scrollables) {
c.addScrollListener((scrollX, scrollY, oldscrollX,
oldscrollY) -> {
// special case for tensile drag
if(scrollY <= 10) {
titleArea.setPreferredSize(null);
MainForm
Regardless of the choice we made above we show it using an animation
titleArea.setPreferredH(0);
}
titleArea.getParent().animateLayout(100);
}
});
for(Container c : scrollables) {
c.addScrollListener((scrollX, scrollY, oldscrollX,
oldscrollY) -> {
// special case for tensile drag
if(scrollY <= 10) {
titleArea.setPreferredSize(null);
return;
}
int diff = oldscrollY - scrollY;
if(diff > 0) {
if(titleArea.getHeight() < titleHeight) {
titleArea.setPreferredH(Math.min(titleHeight,
titleArea.getPreferredH() + diff));
titleArea.setHeight(titleArea.getPreferredH());
titleArea.getParent().revalidate();
}
} else {
if(diff < 0) {
if(titleArea.getHeight() > 0) {
MainForm
We detect the drag operation by binding a scroll listener to the three scrollable containers. I could have used pointer dragged listeners but they might generate too much
noise that isn't applicable.
titleArea.setPreferredH(0);
}
titleArea.getParent().animateLayout(100);
}
});
for(Container c : scrollables) {
c.addScrollListener((scrollX, scrollY, oldscrollX,
oldscrollY) -> {
// special case for tensile drag
if(scrollY <= 10) {
titleArea.setPreferredSize(null);
return;
}
int diff = oldscrollY - scrollY;
if(diff > 0) {
if(titleArea.getHeight() < titleHeight) {
titleArea.setPreferredH(Math.min(titleHeight,
titleArea.getPreferredH() + diff));
titleArea.setHeight(titleArea.getPreferredH());
titleArea.getParent().revalidate();
}
} else {
if(diff < 0) {
if(titleArea.getHeight() > 0) {
MainForm
I chose to make a special case for the tensile drag effect. The tensile effect is the iOS scroll behavior where a drag extends beyond the top most part then bounces back
like a rubber band. This can cause a problem with the logic below so I decided that any scroll position above 10 pixels should probably show the full title
if(scrollY <= 10) {
titleArea.setPreferredSize(null);
return;
}
int diff = oldscrollY - scrollY;
if(diff > 0) {
if(titleArea.getHeight() < titleHeight) {
titleArea.setPreferredH(Math.min(titleHeight,
titleArea.getPreferredH() + diff));
titleArea.setHeight(titleArea.getPreferredH());
titleArea.getParent().revalidate();
}
} else {
if(diff < 0) {
if(titleArea.getHeight() > 0) {
titleArea.setPreferredH(Math.max(0,
titleArea.getPreferredH() + diff));
titleArea.setHeight(titleArea.getPreferredH());
titleArea.getParent().revalidate();
}
}
}
});
MainForm
Now that all of that is out of the way we can calculate the direction of the scroll and shrink/grow the title area appropriately
if(scrollY <= 10) {
titleArea.setPreferredSize(null);
return;
}
int diff = oldscrollY - scrollY;
if(diff > 0) {
if(titleArea.getHeight() < titleHeight) {
titleArea.setPreferredH(Math.min(titleHeight,
titleArea.getPreferredH() + diff));
titleArea.setHeight(titleArea.getPreferredH());
titleArea.getParent().revalidate();
}
} else {
if(diff < 0) {
if(titleArea.getHeight() > 0) {
titleArea.setPreferredH(Math.max(0,
titleArea.getPreferredH() + diff));
titleArea.setHeight(titleArea.getPreferredH());
titleArea.getParent().revalidate();
}
}
}
});
MainForm
If the diff is larger than 0 then the title area should grow. We’re setting the preferred height to the diff plus the preferred height but we make sure not to cross the
maximum height value. We then revalidate to refresh the UI
}
int diff = oldscrollY - scrollY;
if(diff > 0) {
if(titleArea.getHeight() < titleHeight) {
titleArea.setPreferredH(Math.min(titleHeight,
titleArea.getPreferredH() + diff));
titleArea.setHeight(titleArea.getPreferredH());
titleArea.getParent().revalidate();
}
} else {
if(diff < 0) {
if(titleArea.getHeight() > 0) {
titleArea.setPreferredH(Math.max(0,
titleArea.getPreferredH() + diff));
titleArea.setHeight(titleArea.getPreferredH());
titleArea.getParent().revalidate();
}
}
}
});
}
}
MainForm
A negative diff is practically identical with the exception of making it 0 or larger instead of using the minimum value we use the max method. And with that the title folding
is implemented
}
} else {
if(diff < 0) {
if(titleArea.getHeight() > 0) {
titleArea.setPreferredH(Math.max(0,
titleArea.getPreferredH() + diff));
titleArea.setHeight(titleArea.getPreferredH());
titleArea.getParent().revalidate();
}
}
}
});
}
}
@Override
protected void initGlobalToolbar() {
Toolbar tb = new Toolbar();
tb.setTitleCentered(false);
setToolbar(tb);
}
}
MainForm
The one last method in the class is this. We use a custom toolbar that disables centered title. The centered title places the title area in the center of the UI and it doesn’t
work for folding. We need to disable it for this form so the title acts correctly on iOS.

More Related Content

PDF
Creating a Whatsapp Clone - Part V.pdf
PDF
Adapting to Tablets and Desktops - Part 2 - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part IX.pdf
PDF
Creating a Whatsapp Clone - Part IX - Transcript.pdf
PDF
Creating an Uber Clone - Part XXI - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part IV.pdf
PDF
Creating a Facebook Clone - Part XLI - Transcript.pdf
PDF
Creating a Facebook Clone - Part VI - Transcript.pdf
Creating a Whatsapp Clone - Part V.pdf
Adapting to Tablets and Desktops - Part 2 - Transcript.pdf
Creating a Whatsapp Clone - Part IX.pdf
Creating a Whatsapp Clone - Part IX - Transcript.pdf
Creating an Uber Clone - Part XXI - Transcript.pdf
Creating a Whatsapp Clone - Part IV.pdf
Creating a Facebook Clone - Part XLI - Transcript.pdf
Creating a Facebook Clone - Part VI - Transcript.pdf

Similar to Creating a Whatsapp Clone - Part V - Transcript.pdf (20)

PDF
React new features and intro to Hooks
DOCX
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
DOCX
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
PDF
Design pattern - Iterator, Mediator and Memento
PPTX
Advance JFACE
PDF
Creating a Facebook Clone - Part XVI.pdf
DOCX
please code in c#- please note that im a complete beginner- northwind.docx
PDF
Creating a Facebook Clone - Part XXX - Transcript.pdf
PDF
Bot builder v4 HOL
PDF
[C++ gui programming with qt4] chap9
PPTX
PDF
Creating a Whatsapp Clone - Part IV - Transcript.pdf
PDF
Creating a Facebook Clone - Part XXIX - Transcript.pdf
PDF
Extracting ui Design - part 6 - transcript.pdf
PPTX
PDF
Creating a Facebook Clone - Part XXVIII - Transcript.pdf
PDF
JavaScript Refactoring
DOCX
Final_Project
PPTX
The War is Over, and JavaScript has won: Living Under the JS Regime
PDF
Adapting to Tablets and Desktops - Part 2.pdf
React new features and intro to Hooks
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
Design pattern - Iterator, Mediator and Memento
Advance JFACE
Creating a Facebook Clone - Part XVI.pdf
please code in c#- please note that im a complete beginner- northwind.docx
Creating a Facebook Clone - Part XXX - Transcript.pdf
Bot builder v4 HOL
[C++ gui programming with qt4] chap9
Creating a Whatsapp Clone - Part IV - Transcript.pdf
Creating a Facebook Clone - Part XXIX - Transcript.pdf
Extracting ui Design - part 6 - transcript.pdf
Creating a Facebook Clone - Part XXVIII - Transcript.pdf
JavaScript Refactoring
Final_Project
The War is Over, and JavaScript has won: Living Under the JS Regime
Adapting to Tablets and Desktops - Part 2.pdf
Ad

More from ShaiAlmog1 (20)

PDF
The Duck Teaches Learn to debug from the masters. Local to production- kill ...
PDF
create-netflix-clone-06-client-ui.pdf
PDF
create-netflix-clone-01-introduction_transcript.pdf
PDF
create-netflix-clone-02-server_transcript.pdf
PDF
create-netflix-clone-04-server-continued_transcript.pdf
PDF
create-netflix-clone-01-introduction.pdf
PDF
create-netflix-clone-06-client-ui_transcript.pdf
PDF
create-netflix-clone-03-server.pdf
PDF
create-netflix-clone-04-server-continued.pdf
PDF
create-netflix-clone-05-client-model_transcript.pdf
PDF
create-netflix-clone-03-server_transcript.pdf
PDF
create-netflix-clone-02-server.pdf
PDF
create-netflix-clone-05-client-model.pdf
PDF
Creating a Whatsapp Clone - Part II.pdf
PDF
Creating a Whatsapp Clone - Part II - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part I - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part VI.pdf
PDF
Creating a Whatsapp Clone - Part III - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part XI - Transcript.pdf
PDF
Creating a Whatsapp Clone - Part VII.pdf
The Duck Teaches Learn to debug from the masters. Local to production- kill ...
create-netflix-clone-06-client-ui.pdf
create-netflix-clone-01-introduction_transcript.pdf
create-netflix-clone-02-server_transcript.pdf
create-netflix-clone-04-server-continued_transcript.pdf
create-netflix-clone-01-introduction.pdf
create-netflix-clone-06-client-ui_transcript.pdf
create-netflix-clone-03-server.pdf
create-netflix-clone-04-server-continued.pdf
create-netflix-clone-05-client-model_transcript.pdf
create-netflix-clone-03-server_transcript.pdf
create-netflix-clone-02-server.pdf
create-netflix-clone-05-client-model.pdf
Creating a Whatsapp Clone - Part II.pdf
Creating a Whatsapp Clone - Part II - Transcript.pdf
Creating a Whatsapp Clone - Part I - Transcript.pdf
Creating a Whatsapp Clone - Part VI.pdf
Creating a Whatsapp Clone - Part III - Transcript.pdf
Creating a Whatsapp Clone - Part XI - Transcript.pdf
Creating a Whatsapp Clone - Part VII.pdf
Ad

Recently uploaded (20)

PPTX
Effective Security Operations Center (SOC) A Modern, Strategic, and Threat-In...
PDF
KodekX | Application Modernization Development
PPTX
Cloud computing and distributed systems.
PDF
Agricultural_Statistics_at_a_Glance_2022_0.pdf
PDF
The Rise and Fall of 3GPP – Time for a Sabbatical?
PDF
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
PDF
Reach Out and Touch Someone: Haptics and Empathic Computing
PDF
Machine learning based COVID-19 study performance prediction
PDF
Peak of Data & AI Encore- AI for Metadata and Smarter Workflows
PDF
Building Integrated photovoltaic BIPV_UPV.pdf
PPTX
Programs and apps: productivity, graphics, security and other tools
PDF
How UI/UX Design Impacts User Retention in Mobile Apps.pdf
PDF
Blue Purple Modern Animated Computer Science Presentation.pdf.pdf
PDF
MIND Revenue Release Quarter 2 2025 Press Release
PDF
Optimiser vos workloads AI/ML sur Amazon EC2 et AWS Graviton
PDF
Electronic commerce courselecture one. Pdf
PPTX
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
PPTX
MYSQL Presentation for SQL database connectivity
PPT
Teaching material agriculture food technology
PPTX
sap open course for s4hana steps from ECC to s4
Effective Security Operations Center (SOC) A Modern, Strategic, and Threat-In...
KodekX | Application Modernization Development
Cloud computing and distributed systems.
Agricultural_Statistics_at_a_Glance_2022_0.pdf
The Rise and Fall of 3GPP – Time for a Sabbatical?
TokAI - TikTok AI Agent : The First AI Application That Analyzes 10,000+ Vira...
Reach Out and Touch Someone: Haptics and Empathic Computing
Machine learning based COVID-19 study performance prediction
Peak of Data & AI Encore- AI for Metadata and Smarter Workflows
Building Integrated photovoltaic BIPV_UPV.pdf
Programs and apps: productivity, graphics, security and other tools
How UI/UX Design Impacts User Retention in Mobile Apps.pdf
Blue Purple Modern Animated Computer Science Presentation.pdf.pdf
MIND Revenue Release Quarter 2 2025 Press Release
Optimiser vos workloads AI/ML sur Amazon EC2 et AWS Graviton
Electronic commerce courselecture one. Pdf
Detection-First SIEM: Rule Types, Dashboards, and Threat-Informed Strategy
MYSQL Presentation for SQL database connectivity
Teaching material agriculture food technology
sap open course for s4hana steps from ECC to s4

Creating a Whatsapp Clone - Part V - Transcript.pdf

  • 1. Creating a WhatsApp Clone - Part V Now that we implemented the model code and the lifecycle code we are almost finished. We are down to UI code and CSS.
  • 2. public class MainForm extends Form { private Tabs tabs = new Tabs(); private CameraKit ck; private Container chats; private Container status; private Container calls; private static MainForm instance; public MainForm() { super("WhatsApp Clone", new BorderLayout()); instance = this; add(CENTER, tabs); tabs.hideTabs(); ck = CameraKit.create(); tabs.addTab("", createCameraView()); tabs.addTab("", createChatsContainer()); tabs.addTab("", createStatusContainer()); tabs.addTab("", createCallsContainer()); MainForm We start with the main form which covers the list of chat elements. As is the case with all the forms in this app we derive from Form for simplicity.
  • 3. public class MainForm extends Form { private Tabs tabs = new Tabs(); private CameraKit ck; private Container chats; private Container status; private Container calls; private static MainForm instance; public MainForm() { super("WhatsApp Clone", new BorderLayout()); instance = this; add(CENTER, tabs); tabs.hideTabs(); ck = CameraKit.create(); tabs.addTab("", createCameraView()); tabs.addTab("", createChatsContainer()); tabs.addTab("", createStatusContainer()); tabs.addTab("", createCallsContainer()); MainForm The main body of the form is a tabs component that allows us to switch between camera, status and calls
  • 4. public class MainForm extends Form { private Tabs tabs = new Tabs(); private CameraKit ck; private Container chats; private Container status; private Container calls; private static MainForm instance; public MainForm() { super("WhatsApp Clone", new BorderLayout()); instance = this; add(CENTER, tabs); tabs.hideTabs(); ck = CameraKit.create(); tabs.addTab("", createCameraView()); tabs.addTab("", createChatsContainer()); tabs.addTab("", createStatusContainer()); tabs.addTab("", createCallsContainer()); MainForm Camera kit is used to implement the camera UI but due to a regression in the native library this code is currently commented out.
  • 5. public class MainForm extends Form { private Tabs tabs = new Tabs(); private CameraKit ck; private Container chats; private Container status; private Container calls; private static MainForm instance; public MainForm() { super("WhatsApp Clone", new BorderLayout()); instance = this; add(CENTER, tabs); tabs.hideTabs(); ck = CameraKit.create(); tabs.addTab("", createCameraView()); tabs.addTab("", createChatsContainer()); tabs.addTab("", createStatusContainer()); tabs.addTab("", createCallsContainer()); MainForm These are the 3 tab containers, we make use of them in the scrolling logic later
  • 6. public class MainForm extends Form { private Tabs tabs = new Tabs(); private CameraKit ck; private Container chats; private Container status; private Container calls; private static MainForm instance; public MainForm() { super("WhatsApp Clone", new BorderLayout()); instance = this; add(CENTER, tabs); tabs.hideTabs(); ck = CameraKit.create(); tabs.addTab("", createCameraView()); tabs.addTab("", createChatsContainer()); tabs.addTab("", createStatusContainer()); tabs.addTab("", createCallsContainer()); MainForm The main form is a singleton as we need a way to refresh it when we are in a different form
  • 7. private Container status; private Container calls; private static MainForm instance; public MainForm() { super("WhatsApp Clone", new BorderLayout()); instance = this; add(CENTER, tabs); tabs.hideTabs(); ck = CameraKit.create(); tabs.addTab("", createCameraView()); tabs.addTab("", createChatsContainer()); tabs.addTab("", createStatusContainer()); tabs.addTab("", createCallsContainer()); tabs.setSelectedIndex(1); Toolbar tb = getToolbar(); tb.setTitleComponent(createTitleComponent(chats, status, calls)); setBackCommand("", null, e -> { if(tabs.getSelectedIndex() != 1) { tabs.setSelectedIndex(1); } else { MainForm The form itself uses a border layout to place the tabs in the center. We also save the form instance for later use
  • 8. private Container status; private Container calls; private static MainForm instance; public MainForm() { super("WhatsApp Clone", new BorderLayout()); instance = this; add(CENTER, tabs); tabs.hideTabs(); ck = CameraKit.create(); tabs.addTab("", createCameraView()); tabs.addTab("", createChatsContainer()); tabs.addTab("", createStatusContainer()); tabs.addTab("", createCallsContainer()); tabs.setSelectedIndex(1); Toolbar tb = getToolbar(); tb.setTitleComponent(createTitleComponent(chats, status, calls)); setBackCommand("", null, e -> { if(tabs.getSelectedIndex() != 1) { tabs.setSelectedIndex(1); } else { MainForm We hide the tabs, it generally means that These aren't actually tabs, they are buttons we draw ourselves. The reason for that is the special animation we need in the title area
  • 9. private Container status; private Container calls; private static MainForm instance; public MainForm() { super("WhatsApp Clone", new BorderLayout()); instance = this; add(CENTER, tabs); tabs.hideTabs(); ck = CameraKit.create(); tabs.addTab("", createCameraView()); tabs.addTab("", createChatsContainer()); tabs.addTab("", createStatusContainer()); tabs.addTab("", createCallsContainer()); tabs.setSelectedIndex(1); Toolbar tb = getToolbar(); tb.setTitleComponent(createTitleComponent(chats, status, calls)); setBackCommand("", null, e -> { if(tabs.getSelectedIndex() != 1) { tabs.setSelectedIndex(1); } else { MainForm We add the tabs and select the second one as the default as we don’t want to open with the camera view
  • 10. tabs.addTab("", createCameraView()); tabs.addTab("", createChatsContainer()); tabs.addTab("", createStatusContainer()); tabs.addTab("", createCallsContainer()); tabs.setSelectedIndex(1); Toolbar tb = getToolbar(); tb.setTitleComponent(createTitleComponent(chats, status, calls)); setBackCommand("", null, e -> { if(tabs.getSelectedIndex() != 1) { tabs.setSelectedIndex(1); } else { minimizeApplication(); } }); } public static MainForm getInstance() { return instance; } private Container createCallsContainer() { Container cnt = new Container(BoxLayout.y()); MainForm Instead of using the title we use a title component which takes over the entire title area and lets us build whatever we want up there. I’ll discuss it more when covering that method
  • 11. tabs.addTab("", createCameraView()); tabs.addTab("", createChatsContainer()); tabs.addTab("", createStatusContainer()); tabs.addTab("", createCallsContainer()); tabs.setSelectedIndex(1); Toolbar tb = getToolbar(); tb.setTitleComponent(createTitleComponent(chats, status, calls)); setBackCommand("", null, e -> { if(tabs.getSelectedIndex() != 1) { tabs.setSelectedIndex(1); } else { minimizeApplication(); } }); } public static MainForm getInstance() { return instance; } private Container createCallsContainer() { Container cnt = new Container(BoxLayout.y()); MainForm The back command of the form respects the hardware back buttons in some devices and the Android native back arrow. Here we have custom behavior for the form. If we are in a tab other than the first tab we need to return to that tab. Otherwise the app is minimized. This seems to be the behavior of the native app.
  • 12. } public static MainForm getInstance() { return instance; } private Container createCallsContainer() { Container cnt = new Container(BoxLayout.y()); calls = cnt; cnt.setScrollableY(true); MultiButton chat = new MultiButton("Person"); chat.setTextLine2("Date & time"); cnt.add(chat); FloatingActionButton fab = FloatingActionButton. createFAB(FontImage.MATERIAL_CALL); return fab.bindFabToContainer(cnt); } private Container createStatusContainer() { Container cnt = new Container(BoxLayout.y()); status = cnt; cnt.setScrollableY(true); MainForm The calls container is a Y scrollable container.
  • 13. } public static MainForm getInstance() { return instance; } private Container createCallsContainer() { Container cnt = new Container(BoxLayout.y()); calls = cnt; cnt.setScrollableY(true); MultiButton chat = new MultiButton("Person"); chat.setTextLine2("Date & time"); cnt.add(chat); FloatingActionButton fab = FloatingActionButton. createFAB(FontImage.MATERIAL_CALL); return fab.bindFabToContainer(cnt); } private Container createStatusContainer() { Container cnt = new Container(BoxLayout.y()); status = cnt; cnt.setScrollableY(true); MainForm This is simply a placeholder, I placed here a multibutton representing incoming/outgoing calls and a floating action button
  • 14. FloatingActionButton fab = FloatingActionButton. createFAB(FontImage.MATERIAL_CALL); return fab.bindFabToContainer(cnt); } private Container createStatusContainer() { Container cnt = new Container(BoxLayout.y()); status = cnt; cnt.setScrollableY(true); MultiButton chat = new MultiButton("My Status"); chat.setTextLine2("Tap to add status update"); cnt.add(chat); FloatingActionButton fab = FloatingActionButton. createFAB(FontImage.MATERIAL_CAMERA_ALT); return fab.bindFabToContainer(cnt); } public void refreshChatsContainer() { Server.fetchChatList(contacts -> { chats.removeAll(); for(ChatContact c : contacts) { MainForm The same is true for the status container. This isn't an important part of the functionality with this tutorial
  • 15. return fab.bindFabToContainer(cnt); } public void refreshChatsContainer() { Server.fetchChatList(contacts -> { chats.removeAll(); for(ChatContact c : contacts) { MultiButton chat = new MultiButton(c.name.get()); chat.setTextLine2(c.tagline.get()); if(chat.getTextLine2() == null || chat.getTextLine2().length() == 0) { chat.setTextLine2("..."); } chat.setIcon(c.getLargeIcon()); chats.add(chat); chat.addActionListener(e -> new ChatForm(c, this).show()); } chats.revalidate(); }); } private Container createChatsContainer() { MainForm You might recall that we invoke this method from the main UI to refresh the ongoing chat status.
  • 16. return fab.bindFabToContainer(cnt); } public void refreshChatsContainer() { Server.fetchChatList(contacts -> { chats.removeAll(); for(ChatContact c : contacts) { MultiButton chat = new MultiButton(c.name.get()); chat.setTextLine2(c.tagline.get()); if(chat.getTextLine2() == null || chat.getTextLine2().length() == 0) { chat.setTextLine2("..."); } chat.setIcon(c.getLargeIcon()); chats.add(chat); chat.addActionListener(e -> new ChatForm(c, this).show()); } chats.revalidate(); }); } private Container createChatsContainer() { MainForm We fetch up to date data from the storage. This is an asynchronous call that returns on the EDT so the rest of the code goes into the lambda.
  • 17. return fab.bindFabToContainer(cnt); } public void refreshChatsContainer() { Server.fetchChatList(contacts -> { chats.removeAll(); for(ChatContact c : contacts) { MultiButton chat = new MultiButton(c.name.get()); chat.setTextLine2(c.tagline.get()); if(chat.getTextLine2() == null || chat.getTextLine2().length() == 0) { chat.setTextLine2("..."); } chat.setIcon(c.getLargeIcon()); chats.add(chat); chat.addActionListener(e -> new ChatForm(c, this).show()); } chats.revalidate(); }); } private Container createChatsContainer() { MainForm We remove the old content as we’ll just re-add it
  • 18. return fab.bindFabToContainer(cnt); } public void refreshChatsContainer() { Server.fetchChatList(contacts -> { chats.removeAll(); for(ChatContact c : contacts) { MultiButton chat = new MultiButton(c.name.get()); chat.setTextLine2(c.tagline.get()); if(chat.getTextLine2() == null || chat.getTextLine2().length() == 0) { chat.setTextLine2("..."); } chat.setIcon(c.getLargeIcon()); chats.add(chat); chat.addActionListener(e -> new ChatForm(c, this).show()); } chats.revalidate(); }); } private Container createChatsContainer() { MainForm We loop over the contacts and for every new contact we create a chat multi-button with the given name
  • 19. return fab.bindFabToContainer(cnt); } public void refreshChatsContainer() { Server.fetchChatList(contacts -> { chats.removeAll(); for(ChatContact c : contacts) { MultiButton chat = new MultiButton(c.name.get()); chat.setTextLine2(c.tagline.get()); if(chat.getTextLine2() == null || chat.getTextLine2().length() == 0) { chat.setTextLine2("..."); } chat.setIcon(c.getLargeIcon()); chats.add(chat); chat.addActionListener(e -> new ChatForm(c, this).show()); } chats.revalidate(); }); } private Container createChatsContainer() { MainForm If there’s a tagline defined we set that tagline. We also use the large icon for that person
  • 20. return fab.bindFabToContainer(cnt); } public void refreshChatsContainer() { Server.fetchChatList(contacts -> { chats.removeAll(); for(ChatContact c : contacts) { MultiButton chat = new MultiButton(c.name.get()); chat.setTextLine2(c.tagline.get()); if(chat.getTextLine2() == null || chat.getTextLine2().length() == 0) { chat.setTextLine2("..."); } chat.setIcon(c.getLargeIcon()); chats.add(chat); chat.addActionListener(e -> new ChatForm(c, this).show()); } chats.revalidate(); }); } private Container createChatsContainer() { MainForm If the button is clicked we show the chat form for this user
  • 21. chat.addActionListener(e -> new ChatForm(c, this).show()); } chats.revalidate(); }); } private Container createChatsContainer() { chats = new Container(BoxLayout.y()); chats.setScrollableY(true); refreshChatsContainer(); FloatingActionButton fab = FloatingActionButton. createFAB(FontImage.MATERIAL_CHAT); fab.addActionListener(e -> new NewMessageForm().show()); return fab.bindFabToContainer(chats); } private Container createCameraView() { if(ck != null) { Container cameraCnt = new Container(new LayeredLayout()); MainForm The chats container is the same as the other containers we saw but it’s actually fully implemented
  • 22. chat.addActionListener(e -> new ChatForm(c, this).show()); } chats.revalidate(); }); } private Container createChatsContainer() { chats = new Container(BoxLayout.y()); chats.setScrollableY(true); refreshChatsContainer(); FloatingActionButton fab = FloatingActionButton. createFAB(FontImage.MATERIAL_CHAT); fab.addActionListener(e -> new NewMessageForm().show()); return fab.bindFabToContainer(chats); } private Container createCameraView() { if(ck != null) { Container cameraCnt = new Container(new LayeredLayout()); MainForm It invokes the refresh chats container method we previously saw in order to fill up the container
  • 23. chat.addActionListener(e -> new ChatForm(c, this).show()); } chats.revalidate(); }); } private Container createChatsContainer() { chats = new Container(BoxLayout.y()); chats.setScrollableY(true); refreshChatsContainer(); FloatingActionButton fab = FloatingActionButton. createFAB(FontImage.MATERIAL_CHAT); fab.addActionListener(e -> new NewMessageForm().show()); return fab.bindFabToContainer(chats); } private Container createCameraView() { if(ck != null) { Container cameraCnt = new Container(new LayeredLayout()); MainForm The floating action button here is actually implemented by showing the new message form
  • 24. } private Container createCameraView() { if(ck != null) { Container cameraCnt = new Container(new LayeredLayout()); tabs.addSelectionListener((oldSelected, newSelected) -> { if(newSelected == 0) { //ck.start(); //cameraCnt.add(ck.getView()); getToolbar().setHidden(true); } else { if(oldSelected == 0) { //cameraCnt.removeAll(); //ck.stop(); getToolbar().setHidden(false); } } }); return cameraCnt; } return BorderLayout.center(new Label("Camera Unsupported")); } MainForm Camera support is currently commented out due to a regression in the native library. However, the concept is relatively simple. We use the tab selection listener to activate the camera as we need it
  • 25. } return BorderLayout.center(new Label("Camera Unsupported")); } private void showOverflowMenu() { Button newGroup = new Button("New group", "Command"); Button newBroadcast = new Button("New broadcast", "Command"); Button whatsappWeb = new Button("WhatsApp Web", "Command"); Button starred = new Button("Starred Messages", "Command"); Button settings = new Button("Settings", "Command"); Container cnt = BoxLayout.encloseY(newGroup, newBroadcast, whatsappWeb, starred, settings); cnt.setUIID("CommandList"); Dialog dlg = new Dialog(new BorderLayout()); dlg.setDialogUIID("Container"); dlg.add(CENTER, cnt); dlg.setDisposeWhenPointerOutOfBounds(true); dlg.setTransitionInAnimator(CommonTransitions.createEmpty()); dlg.setTransitionOutAnimator(CommonTransitions.createEmpty()); dlg.setBackCommand("", null, e -> dlg.dispose()); int top = getUIManager().getComponentStyle("StatusBar"). getVerticalPadding(); setTintColor(0); int bottom = getHeight() - cnt.getPreferredH() - top - MainForm The overflow menu is normally implemented in the toolbar but since I wanted more control over the toolbar area I chose to implement i manually in the code.
  • 26. } return BorderLayout.center(new Label("Camera Unsupported")); } private void showOverflowMenu() { Button newGroup = new Button("New group", "Command"); Button newBroadcast = new Button("New broadcast", "Command"); Button whatsappWeb = new Button("WhatsApp Web", "Command"); Button starred = new Button("Starred Messages", "Command"); Button settings = new Button("Settings", "Command"); Container cnt = BoxLayout.encloseY(newGroup, newBroadcast, whatsappWeb, starred, settings); cnt.setUIID("CommandList"); Dialog dlg = new Dialog(new BorderLayout()); dlg.setDialogUIID("Container"); dlg.add(CENTER, cnt); dlg.setDisposeWhenPointerOutOfBounds(true); dlg.setTransitionInAnimator(CommonTransitions.createEmpty()); dlg.setTransitionOutAnimator(CommonTransitions.createEmpty()); dlg.setBackCommand("", null, e -> dlg.dispose()); int top = getUIManager().getComponentStyle("StatusBar"). getVerticalPadding(); setTintColor(0); int bottom = getHeight() - cnt.getPreferredH() - top - MainForm I used buttons with the Command UIID and a Container with the CommandList UIID to create this UI. I used buttons with the Command UIID and a Container with the CommandList UIID to create this UI. I’ll discuss the CSS that created this in the next lesson.
  • 27. Button whatsappWeb = new Button("WhatsApp Web", "Command"); Button starred = new Button("Starred Messages", "Command"); Button settings = new Button("Settings", "Command"); Container cnt = BoxLayout.encloseY(newGroup, newBroadcast, whatsappWeb, starred, settings); cnt.setUIID("CommandList"); Dialog dlg = new Dialog(new BorderLayout()); dlg.setDialogUIID("Container"); dlg.add(CENTER, cnt); dlg.setDisposeWhenPointerOutOfBounds(true); dlg.setTransitionInAnimator(CommonTransitions.createEmpty()); dlg.setTransitionOutAnimator(CommonTransitions.createEmpty()); dlg.setBackCommand("", null, e -> dlg.dispose()); int top = getUIManager().getComponentStyle("StatusBar"). getVerticalPadding(); setTintColor(0); int bottom = getHeight() - cnt.getPreferredH() - top - cnt.getUnselectedStyle().getVerticalPadding() - cnt.getUnselectedStyle().getVerticalMargins(); int w = getWidth(); int left = w - cnt.getPreferredW() - cnt.getUnselectedStyle().getHorizontalPadding() - cnt.getUnselectedStyle().getHorizontalMargins(); dlg.show(top, bottom, left, 0); MainForm I create a transparent dialog by giving it the Container UIID. I place the menu in the center
  • 28. Button whatsappWeb = new Button("WhatsApp Web", "Command"); Button starred = new Button("Starred Messages", "Command"); Button settings = new Button("Settings", "Command"); Container cnt = BoxLayout.encloseY(newGroup, newBroadcast, whatsappWeb, starred, settings); cnt.setUIID("CommandList"); Dialog dlg = new Dialog(new BorderLayout()); dlg.setDialogUIID("Container"); dlg.add(CENTER, cnt); dlg.setDisposeWhenPointerOutOfBounds(true); dlg.setTransitionInAnimator(CommonTransitions.createEmpty()); dlg.setTransitionOutAnimator(CommonTransitions.createEmpty()); dlg.setBackCommand("", null, e -> dlg.dispose()); int top = getUIManager().getComponentStyle("StatusBar"). getVerticalPadding(); setTintColor(0); int bottom = getHeight() - cnt.getPreferredH() - top - cnt.getUnselectedStyle().getVerticalPadding() - cnt.getUnselectedStyle().getVerticalMargins(); int w = getWidth(); int left = w - cnt.getPreferredW() - cnt.getUnselectedStyle().getHorizontalPadding() - cnt.getUnselectedStyle().getHorizontalMargins(); dlg.show(top, bottom, left, 0); MainForm The dialog has no transition and disposed if the user taps outside of it or uses the back button
  • 29. whatsappWeb, starred, settings); cnt.setUIID("CommandList"); Dialog dlg = new Dialog(new BorderLayout()); dlg.setDialogUIID("Container"); dlg.add(CENTER, cnt); dlg.setDisposeWhenPointerOutOfBounds(true); dlg.setTransitionInAnimator(CommonTransitions.createEmpty()); dlg.setTransitionOutAnimator(CommonTransitions.createEmpty()); dlg.setBackCommand("", null, e -> dlg.dispose()); int top = getUIManager().getComponentStyle("StatusBar"). getVerticalPadding(); setTintColor(0); int bottom = getHeight() - cnt.getPreferredH() - top - cnt.getUnselectedStyle().getVerticalPadding() - cnt.getUnselectedStyle().getVerticalMargins(); int w = getWidth(); int left = w - cnt.getPreferredW() - cnt.getUnselectedStyle().getHorizontalPadding() - cnt.getUnselectedStyle().getHorizontalMargins(); dlg.show(top, bottom, left, 0); } private Container createTitleComponent(Container... scrollables) { Label title = new Label("WhatsApp", "Title"); MainForm This disables the default darkening of the form when a dialog is shown
  • 30. whatsappWeb, starred, settings); cnt.setUIID("CommandList"); Dialog dlg = new Dialog(new BorderLayout()); dlg.setDialogUIID("Container"); dlg.add(CENTER, cnt); dlg.setDisposeWhenPointerOutOfBounds(true); dlg.setTransitionInAnimator(CommonTransitions.createEmpty()); dlg.setTransitionOutAnimator(CommonTransitions.createEmpty()); dlg.setBackCommand("", null, e -> dlg.dispose()); int top = getUIManager().getComponentStyle("StatusBar"). getVerticalPadding(); setTintColor(0); int bottom = getHeight() - cnt.getPreferredH() - top - cnt.getUnselectedStyle().getVerticalPadding() - cnt.getUnselectedStyle().getVerticalMargins(); int w = getWidth(); int left = w - cnt.getPreferredW() - cnt.getUnselectedStyle().getHorizontalPadding() - cnt.getUnselectedStyle().getHorizontalMargins(); dlg.show(top, bottom, left, 0); } private Container createTitleComponent(Container... scrollables) { Label title = new Label("WhatsApp", "Title"); MainForm This version of the show method places the dialog with a fixed distance from the edges. We give it a small margin on the top to take the status bar into account. Then use left and bottom margin to push the dialog to the top right side. This gives us a lot of flexibility and allows us to show the dialog in any way we want.
  • 31. cnt.getUnselectedStyle().getVerticalPadding() - cnt.getUnselectedStyle().getVerticalMargins(); int w = getWidth(); int left = w - cnt.getPreferredW() - cnt.getUnselectedStyle().getHorizontalPadding() - cnt.getUnselectedStyle().getHorizontalMargins(); dlg.show(top, bottom, left, 0); } private Container createTitleComponent(Container... scrollables) { Label title = new Label("WhatsApp", "Title"); Container titleArea; if(title.getUnselectedStyle().getAlignment() == LEFT) { titleArea = BorderLayout.center(title); } else { // for iOS we want the title to center properly titleArea = BorderLayout.centerAbsolute(title); } Button search = new Button("", FontImage.MATERIAL_SEARCH, "Title"); Button overflow = new Button("", FontImage.MATERIAL_MORE_VERT, "Title"); overflow.addActionListener(e -> showOverflowMenu()); titleArea.add(EAST, GridLayout.encloseIn(2, search, overflow)); MainForm This method creates the title component for the form which is this region. The method accepts the scrollable containers in the tabs container. This allows us to track scrolling and seamlessly fold the title area
  • 32. cnt.getUnselectedStyle().getVerticalPadding() - cnt.getUnselectedStyle().getVerticalMargins(); int w = getWidth(); int left = w - cnt.getPreferredW() - cnt.getUnselectedStyle().getHorizontalPadding() - cnt.getUnselectedStyle().getHorizontalMargins(); dlg.show(top, bottom, left, 0); } private Container createTitleComponent(Container... scrollables) { Label title = new Label("WhatsApp", "Title"); Container titleArea; if(title.getUnselectedStyle().getAlignment() == LEFT) { titleArea = BorderLayout.center(title); } else { // for iOS we want the title to center properly titleArea = BorderLayout.centerAbsolute(title); } Button search = new Button("", FontImage.MATERIAL_SEARCH, "Title"); Button overflow = new Button("", FontImage.MATERIAL_MORE_VERT, "Title"); overflow.addActionListener(e -> showOverflowMenu()); titleArea.add(EAST, GridLayout.encloseIn(2, search, overflow)); MainForm The title itself is just a label with the “Title” UIID. It’s placed in the center of the title area border layout. If we are on iOS we want the title to be centered, in that case we need to use the center version of the border layout. The reason for this is that center alignment doesn’t know about the full layout and would center based on available space. It would ignore the search and overflow buttons on the right when centering since it isn’t aware of other components. However, using the center alignment and placing these buttons in the east solves that problem and gives us the correct title position.
  • 33. if(title.getUnselectedStyle().getAlignment() == LEFT) { titleArea = BorderLayout.center(title); } else { // for iOS we want the title to center properly titleArea = BorderLayout.centerAbsolute(title); } Button search = new Button("", FontImage.MATERIAL_SEARCH, "Title"); Button overflow = new Button("", FontImage.MATERIAL_MORE_VERT, "Title"); overflow.addActionListener(e -> showOverflowMenu()); titleArea.add(EAST, GridLayout.encloseIn(2, search, overflow)); ButtonGroup bg = new ButtonGroup(); RadioButton camera = RadioButton.createToggle("", bg); camera.setUIID("SubTitle"); FontImage.setMaterialIcon(camera, FontImage.MATERIAL_CAMERA_ALT); RadioButton chats = RadioButton.createToggle("Chats", bg); RadioButton status = RadioButton.createToggle("Status", bg); RadioButton calls = RadioButton.createToggle("Calls", bg); chats.setUIID("SubTitle"); status.setUIID("SubTitle"); calls.setUIID("SubTitle"); RadioButton[] buttons = new RadioButton[] { MainForm The search and overflow commands are just buttons with the “Title” UIID. We already discussed the showOverflowMenu() method so this should be pretty obvious. We just place the two buttons in the grid. I chose not to use a Command as this might create a misalignment for this use case and wouldn’t have saved on the amount of code I had to write.
  • 34. Button search = new Button("", FontImage.MATERIAL_SEARCH, "Title"); Button overflow = new Button("", FontImage.MATERIAL_MORE_VERT, "Title"); overflow.addActionListener(e -> showOverflowMenu()); titleArea.add(EAST, GridLayout.encloseIn(2, search, overflow)); ButtonGroup bg = new ButtonGroup(); RadioButton camera = RadioButton.createToggle("", bg); camera.setUIID("SubTitle"); FontImage.setMaterialIcon(camera, FontImage.MATERIAL_CAMERA_ALT); RadioButton chats = RadioButton.createToggle("Chats", bg); RadioButton status = RadioButton.createToggle("Status", bg); RadioButton calls = RadioButton.createToggle("Calls", bg); chats.setUIID("SubTitle"); status.setUIID("SubTitle"); calls.setUIID("SubTitle"); RadioButton[] buttons = new RadioButton[] { camera, chats, status, calls }; TableLayout tb = new TableLayout(2, 4); Container toggles = new Container(tb); MainForm These are the tabs for selecting camera, chat etc… They are just toggle buttons which in this case are classified as radio buttons. This means only one of the radio buttons within the button group can be selected. We give them all the SubTitle UIID which again I’ll discuss in the next lesson.
  • 35. chats.setUIID("SubTitle"); status.setUIID("SubTitle"); calls.setUIID("SubTitle"); RadioButton[] buttons = new RadioButton[] { camera, chats, status, calls }; TableLayout tb = new TableLayout(2, 4); Container toggles = new Container(tb); toggles.add(tb.createConstraint().widthPercentage(10), camera); toggles.add(tb.createConstraint().widthPercentage(30), chats); toggles.add(tb.createConstraint().widthPercentage(30), status); toggles.add(tb.createConstraint().widthPercentage(30), calls); Label whiteLine = new Label("", "SubTitleUnderline"); whiteLine.setShowEvenIfBlank(true); toggles.add(tb.createConstraint(1, 1) ,whiteLine); final Container finalTitle = titleArea; for(int iter = 0 ; iter < buttons.length ; iter++) { final int current = iter; buttons[iter].addActionListener(e -> { tabs.setSelectedIndex(current); MainForm We use table layout to place the tabs into the UI, this allows us to explicitly determine the width of the columns. Notice that the table layout has 2 rows…
  • 36. TableLayout tb = new TableLayout(2, 4); Container toggles = new Container(tb); toggles.add(tb.createConstraint().widthPercentage(10), camera); toggles.add(tb.createConstraint().widthPercentage(30), chats); toggles.add(tb.createConstraint().widthPercentage(30), status); toggles.add(tb.createConstraint().widthPercentage(30), calls); Label whiteLine = new Label("", "SubTitleUnderline"); whiteLine.setShowEvenIfBlank(true); toggles.add(tb.createConstraint(1, 1) ,whiteLine); final Container finalTitle = titleArea; for(int iter = 0 ; iter < buttons.length ; iter++) { final int current = iter; buttons[iter].addActionListener(e -> { tabs.setSelectedIndex(current); whiteLine.remove(); toggles.add(tb.createConstraint(1, current) ,whiteLine); finalTitle.setPreferredSize(null); toggles.animateLayout(100); }); } MainForm The second row of the table contains a white line using the “SideTitleUnderline” UIID. This line is placed in row one and column one so it’s under the chats entry. When we move between the tabs this underline needs to animate to the new position.
  • 37. Label whiteLine = new Label("", "SubTitleUnderline"); whiteLine.setShowEvenIfBlank(true); toggles.add(tb.createConstraint(1, 1) ,whiteLine); final Container finalTitle = titleArea; for(int iter = 0 ; iter < buttons.length ; iter++) { final int current = iter; buttons[iter].addActionListener(e -> { tabs.setSelectedIndex(current); whiteLine.remove(); toggles.add(tb.createConstraint(1, current) ,whiteLine); finalTitle.setPreferredSize(null); toggles.animateLayout(100); }); } tabs.addSelectionListener((oldSelected, newSelected) -> { if(!buttons[newSelected].isSelected()) { finalTitle.setPreferredSize(null); buttons[newSelected].setSelected(true); whiteLine.remove(); toggles.add(tb.createConstraint(1, newSelected) ,whiteLine); toggles.animateLayout(100); MainForm Here we bind listeners to all the four buttons mapping to each tab.
  • 38. Label whiteLine = new Label("", "SubTitleUnderline"); whiteLine.setShowEvenIfBlank(true); toggles.add(tb.createConstraint(1, 1) ,whiteLine); final Container finalTitle = titleArea; for(int iter = 0 ; iter < buttons.length ; iter++) { final int current = iter; buttons[iter].addActionListener(e -> { tabs.setSelectedIndex(current); whiteLine.remove(); toggles.add(tb.createConstraint(1, current) ,whiteLine); finalTitle.setPreferredSize(null); toggles.animateLayout(100); }); } tabs.addSelectionListener((oldSelected, newSelected) -> { if(!buttons[newSelected].isSelected()) { finalTitle.setPreferredSize(null); buttons[newSelected].setSelected(true); whiteLine.remove(); toggles.add(tb.createConstraint(1, newSelected) ,whiteLine); toggles.animateLayout(100); MainForm When a button is clicked we select the appropriate tab
  • 39. Label whiteLine = new Label("", "SubTitleUnderline"); whiteLine.setShowEvenIfBlank(true); toggles.add(tb.createConstraint(1, 1) ,whiteLine); final Container finalTitle = titleArea; for(int iter = 0 ; iter < buttons.length ; iter++) { final int current = iter; buttons[iter].addActionListener(e -> { tabs.setSelectedIndex(current); whiteLine.remove(); toggles.add(tb.createConstraint(1, current) ,whiteLine); finalTitle.setPreferredSize(null); toggles.animateLayout(100); }); } tabs.addSelectionListener((oldSelected, newSelected) -> { if(!buttons[newSelected].isSelected()) { finalTitle.setPreferredSize(null); buttons[newSelected].setSelected(true); whiteLine.remove(); toggles.add(tb.createConstraint(1, newSelected) ,whiteLine); toggles.animateLayout(100); MainForm The next two lines implement the underline animation effect that we see when we click a button. Notice how the line animates to the right tab button. To achieve this we remove the current white line and add it back to the toggle container in the right position.
  • 40. Label whiteLine = new Label("", "SubTitleUnderline"); whiteLine.setShowEvenIfBlank(true); toggles.add(tb.createConstraint(1, 1) ,whiteLine); final Container finalTitle = titleArea; for(int iter = 0 ; iter < buttons.length ; iter++) { final int current = iter; buttons[iter].addActionListener(e -> { tabs.setSelectedIndex(current); whiteLine.remove(); toggles.add(tb.createConstraint(1, current) ,whiteLine); finalTitle.setPreferredSize(null); toggles.animateLayout(100); }); } tabs.addSelectionListener((oldSelected, newSelected) -> { if(!buttons[newSelected].isSelected()) { finalTitle.setPreferredSize(null); buttons[newSelected].setSelected(true); whiteLine.remove(); toggles.add(tb.createConstraint(1, newSelected) ,whiteLine); toggles.animateLayout(100); MainForm We reset the height of the title in case it was shrunk during scrolling
  • 41. Label whiteLine = new Label("", "SubTitleUnderline"); whiteLine.setShowEvenIfBlank(true); toggles.add(tb.createConstraint(1, 1) ,whiteLine); final Container finalTitle = titleArea; for(int iter = 0 ; iter < buttons.length ; iter++) { final int current = iter; buttons[iter].addActionListener(e -> { tabs.setSelectedIndex(current); whiteLine.remove(); toggles.add(tb.createConstraint(1, current) ,whiteLine); finalTitle.setPreferredSize(null); toggles.animateLayout(100); }); } tabs.addSelectionListener((oldSelected, newSelected) -> { if(!buttons[newSelected].isSelected()) { finalTitle.setPreferredSize(null); buttons[newSelected].setSelected(true); whiteLine.remove(); toggles.add(tb.createConstraint(1, newSelected) ,whiteLine); toggles.animateLayout(100); MainForm And we finally update the layout with an animation which performs the actual line move animation
  • 42. whiteLine.remove(); toggles.add(tb.createConstraint(1, current) ,whiteLine); finalTitle.setPreferredSize(null); toggles.animateLayout(100); }); } tabs.addSelectionListener((oldSelected, newSelected) -> { if(!buttons[newSelected].isSelected()) { finalTitle.setPreferredSize(null); buttons[newSelected].setSelected(true); whiteLine.remove(); toggles.add(tb.createConstraint(1, newSelected) ,whiteLine); toggles.animateLayout(100); } }); bindFolding(titleArea, titleArea.getPreferredH(), scrollables); return BoxLayout.encloseY(titleArea, toggles); } private void bindFolding(Container titleArea, int titleHeight, Container... scrollables) { MainForm The previous block updated the tab selection when we select a button. This block does the opposite. It updates the button selection when we swipe the tabs. It uses a tab selection listener
  • 43. whiteLine.remove(); toggles.add(tb.createConstraint(1, current) ,whiteLine); finalTitle.setPreferredSize(null); toggles.animateLayout(100); }); } tabs.addSelectionListener((oldSelected, newSelected) -> { if(!buttons[newSelected].isSelected()) { finalTitle.setPreferredSize(null); buttons[newSelected].setSelected(true); whiteLine.remove(); toggles.add(tb.createConstraint(1, newSelected) ,whiteLine); toggles.animateLayout(100); } }); bindFolding(titleArea, titleArea.getPreferredH(), scrollables); return BoxLayout.encloseY(titleArea, toggles); } private void bindFolding(Container titleArea, int titleHeight, Container... scrollables) { MainForm If the button isn't selected then we need to update it
  • 44. whiteLine.remove(); toggles.add(tb.createConstraint(1, current) ,whiteLine); finalTitle.setPreferredSize(null); toggles.animateLayout(100); }); } tabs.addSelectionListener((oldSelected, newSelected) -> { if(!buttons[newSelected].isSelected()) { finalTitle.setPreferredSize(null); buttons[newSelected].setSelected(true); whiteLine.remove(); toggles.add(tb.createConstraint(1, newSelected) ,whiteLine); toggles.animateLayout(100); } }); bindFolding(titleArea, titleArea.getPreferredH(), scrollables); return BoxLayout.encloseY(titleArea, toggles); } private void bindFolding(Container titleArea, int titleHeight, Container... scrollables) { MainForm Again we need to reset the title size
  • 45. whiteLine.remove(); toggles.add(tb.createConstraint(1, current) ,whiteLine); finalTitle.setPreferredSize(null); toggles.animateLayout(100); }); } tabs.addSelectionListener((oldSelected, newSelected) -> { if(!buttons[newSelected].isSelected()) { finalTitle.setPreferredSize(null); buttons[newSelected].setSelected(true); whiteLine.remove(); toggles.add(tb.createConstraint(1, newSelected) ,whiteLine); toggles.animateLayout(100); } }); bindFolding(titleArea, titleArea.getPreferredH(), scrollables); return BoxLayout.encloseY(titleArea, toggles); } private void bindFolding(Container titleArea, int titleHeight, Container... scrollables) { MainForm Next we select the button that matches the tab
  • 46. tabs.addSelectionListener((oldSelected, newSelected) -> { if(!buttons[newSelected].isSelected()) { finalTitle.setPreferredSize(null); buttons[newSelected].setSelected(true); whiteLine.remove(); toggles.add(tb.createConstraint(1, newSelected) ,whiteLine); toggles.animateLayout(100); } }); bindFolding(titleArea, titleArea.getPreferredH(), scrollables); return BoxLayout.encloseY(titleArea, toggles); } private void bindFolding(Container titleArea, int titleHeight, Container... scrollables) { addPointerReleasedListener(e -> { if(titleArea.getHeight() != titleHeight && titleArea.getHeight() != 0) { if(titleHeight - titleArea.getHeight() > titleHeight / 2) { titleArea.setPreferredSize(null); MainForm Finally we perform the animation of moving the underline between the tabs. Notice that this is almost identical to the previous animation code only in this case it’s triggered by a dragging of the tabs instead of the button click event.
  • 47. tabs.addSelectionListener((oldSelected, newSelected) -> { if(!buttons[newSelected].isSelected()) { finalTitle.setPreferredSize(null); buttons[newSelected].setSelected(true); whiteLine.remove(); toggles.add(tb.createConstraint(1, newSelected) ,whiteLine); toggles.animateLayout(100); } }); bindFolding(titleArea, titleArea.getPreferredH(), scrollables); return BoxLayout.encloseY(titleArea, toggles); } private void bindFolding(Container titleArea, int titleHeight, Container... scrollables) { addPointerReleasedListener(e -> { if(titleArea.getHeight() != titleHeight && titleArea.getHeight() != 0) { if(titleHeight - titleArea.getHeight() > titleHeight / 2) { titleArea.setPreferredSize(null); MainForm The last two lines in this method are the bindFolding call which we will discuss soon and the box layout Y which wraps the two containers as one.
  • 48. bindFolding(titleArea, titleArea.getPreferredH(), scrollables); return BoxLayout.encloseY(titleArea, toggles); } private void bindFolding(Container titleArea, int titleHeight, Container... scrollables) { addPointerReleasedListener(e -> { if(titleArea.getHeight() != titleHeight && titleArea.getHeight() != 0) { if(titleHeight - titleArea.getHeight() > titleHeight / 2) { titleArea.setPreferredSize(null); } else { titleArea.setPreferredH(0); } titleArea.getParent().animateLayout(100); } }); for(Container c : scrollables) { c.addScrollListener((scrollX, scrollY, oldscrollX, oldscrollY) -> { // special case for tensile drag if(scrollY <= 10) { titleArea.setPreferredSize(null); MainForm The bindFolding method implements this animation of folding title. It's implemented by tracking pointer drag events and shrinking the title.
  • 49. bindFolding(titleArea, titleArea.getPreferredH(), scrollables); return BoxLayout.encloseY(titleArea, toggles); } private void bindFolding(Container titleArea, int titleHeight, Container... scrollables) { addPointerReleasedListener(e -> { if(titleArea.getHeight() != titleHeight && titleArea.getHeight() != 0) { if(titleHeight - titleArea.getHeight() > titleHeight / 2) { titleArea.setPreferredSize(null); } else { titleArea.setPreferredH(0); } titleArea.getParent().animateLayout(100); } }); for(Container c : scrollables) { c.addScrollListener((scrollX, scrollY, oldscrollX, oldscrollY) -> { // special case for tensile drag if(scrollY <= 10) { titleArea.setPreferredSize(null); MainForm When the pointer is released we need to check if the title shrunk enough to minimize or not enough so it would go back to the full size.
  • 50. bindFolding(titleArea, titleArea.getPreferredH(), scrollables); return BoxLayout.encloseY(titleArea, toggles); } private void bindFolding(Container titleArea, int titleHeight, Container... scrollables) { addPointerReleasedListener(e -> { if(titleArea.getHeight() != titleHeight && titleArea.getHeight() != 0) { if(titleHeight - titleArea.getHeight() > titleHeight / 2) { titleArea.setPreferredSize(null); } else { titleArea.setPreferredH(0); } titleArea.getParent().animateLayout(100); } }); for(Container c : scrollables) { c.addScrollListener((scrollX, scrollY, oldscrollX, oldscrollY) -> { // special case for tensile drag if(scrollY <= 10) { titleArea.setPreferredSize(null); MainForm If the title area height is different from the original height it means we are in the process of shrinking the title.
  • 51. bindFolding(titleArea, titleArea.getPreferredH(), scrollables); return BoxLayout.encloseY(titleArea, toggles); } private void bindFolding(Container titleArea, int titleHeight, Container... scrollables) { addPointerReleasedListener(e -> { if(titleArea.getHeight() != titleHeight && titleArea.getHeight() != 0) { if(titleHeight - titleArea.getHeight() > titleHeight / 2) { titleArea.setPreferredSize(null); } else { titleArea.setPreferredH(0); } titleArea.getParent().animateLayout(100); } }); for(Container c : scrollables) { c.addScrollListener((scrollX, scrollY, oldscrollX, oldscrollY) -> { // special case for tensile drag if(scrollY <= 10) { titleArea.setPreferredSize(null); MainForm In that case we need to decide whether the process is closer to the finish line or to the start
  • 52. bindFolding(titleArea, titleArea.getPreferredH(), scrollables); return BoxLayout.encloseY(titleArea, toggles); } private void bindFolding(Container titleArea, int titleHeight, Container... scrollables) { addPointerReleasedListener(e -> { if(titleArea.getHeight() != titleHeight && titleArea.getHeight() != 0) { if(titleHeight - titleArea.getHeight() > titleHeight / 2) { titleArea.setPreferredSize(null); } else { titleArea.setPreferredH(0); } titleArea.getParent().animateLayout(100); } }); for(Container c : scrollables) { c.addScrollListener((scrollX, scrollY, oldscrollX, oldscrollY) -> { // special case for tensile drag if(scrollY <= 10) { titleArea.setPreferredSize(null); MainForm If it’s less than half way to the height of the title we reset the preferred size of the title area. That means the title area will take up it’s original preferred size and grow back to full height
  • 53. bindFolding(titleArea, titleArea.getPreferredH(), scrollables); return BoxLayout.encloseY(titleArea, toggles); } private void bindFolding(Container titleArea, int titleHeight, Container... scrollables) { addPointerReleasedListener(e -> { if(titleArea.getHeight() != titleHeight && titleArea.getHeight() != 0) { if(titleHeight - titleArea.getHeight() > titleHeight / 2) { titleArea.setPreferredSize(null); } else { titleArea.setPreferredH(0); } titleArea.getParent().animateLayout(100); } }); for(Container c : scrollables) { c.addScrollListener((scrollX, scrollY, oldscrollX, oldscrollY) -> { // special case for tensile drag if(scrollY <= 10) { titleArea.setPreferredSize(null); MainForm Otherwise we set the title area height to 0 so it’s effectively hidden
  • 54. bindFolding(titleArea, titleArea.getPreferredH(), scrollables); return BoxLayout.encloseY(titleArea, toggles); } private void bindFolding(Container titleArea, int titleHeight, Container... scrollables) { addPointerReleasedListener(e -> { if(titleArea.getHeight() != titleHeight && titleArea.getHeight() != 0) { if(titleHeight - titleArea.getHeight() > titleHeight / 2) { titleArea.setPreferredSize(null); } else { titleArea.setPreferredH(0); } titleArea.getParent().animateLayout(100); } }); for(Container c : scrollables) { c.addScrollListener((scrollX, scrollY, oldscrollX, oldscrollY) -> { // special case for tensile drag if(scrollY <= 10) { titleArea.setPreferredSize(null); MainForm Regardless of the choice we made above we show it using an animation
  • 55. titleArea.setPreferredH(0); } titleArea.getParent().animateLayout(100); } }); for(Container c : scrollables) { c.addScrollListener((scrollX, scrollY, oldscrollX, oldscrollY) -> { // special case for tensile drag if(scrollY <= 10) { titleArea.setPreferredSize(null); return; } int diff = oldscrollY - scrollY; if(diff > 0) { if(titleArea.getHeight() < titleHeight) { titleArea.setPreferredH(Math.min(titleHeight, titleArea.getPreferredH() + diff)); titleArea.setHeight(titleArea.getPreferredH()); titleArea.getParent().revalidate(); } } else { if(diff < 0) { if(titleArea.getHeight() > 0) { MainForm We detect the drag operation by binding a scroll listener to the three scrollable containers. I could have used pointer dragged listeners but they might generate too much noise that isn't applicable.
  • 56. titleArea.setPreferredH(0); } titleArea.getParent().animateLayout(100); } }); for(Container c : scrollables) { c.addScrollListener((scrollX, scrollY, oldscrollX, oldscrollY) -> { // special case for tensile drag if(scrollY <= 10) { titleArea.setPreferredSize(null); return; } int diff = oldscrollY - scrollY; if(diff > 0) { if(titleArea.getHeight() < titleHeight) { titleArea.setPreferredH(Math.min(titleHeight, titleArea.getPreferredH() + diff)); titleArea.setHeight(titleArea.getPreferredH()); titleArea.getParent().revalidate(); } } else { if(diff < 0) { if(titleArea.getHeight() > 0) { MainForm I chose to make a special case for the tensile drag effect. The tensile effect is the iOS scroll behavior where a drag extends beyond the top most part then bounces back like a rubber band. This can cause a problem with the logic below so I decided that any scroll position above 10 pixels should probably show the full title
  • 57. if(scrollY <= 10) { titleArea.setPreferredSize(null); return; } int diff = oldscrollY - scrollY; if(diff > 0) { if(titleArea.getHeight() < titleHeight) { titleArea.setPreferredH(Math.min(titleHeight, titleArea.getPreferredH() + diff)); titleArea.setHeight(titleArea.getPreferredH()); titleArea.getParent().revalidate(); } } else { if(diff < 0) { if(titleArea.getHeight() > 0) { titleArea.setPreferredH(Math.max(0, titleArea.getPreferredH() + diff)); titleArea.setHeight(titleArea.getPreferredH()); titleArea.getParent().revalidate(); } } } }); MainForm Now that all of that is out of the way we can calculate the direction of the scroll and shrink/grow the title area appropriately
  • 58. if(scrollY <= 10) { titleArea.setPreferredSize(null); return; } int diff = oldscrollY - scrollY; if(diff > 0) { if(titleArea.getHeight() < titleHeight) { titleArea.setPreferredH(Math.min(titleHeight, titleArea.getPreferredH() + diff)); titleArea.setHeight(titleArea.getPreferredH()); titleArea.getParent().revalidate(); } } else { if(diff < 0) { if(titleArea.getHeight() > 0) { titleArea.setPreferredH(Math.max(0, titleArea.getPreferredH() + diff)); titleArea.setHeight(titleArea.getPreferredH()); titleArea.getParent().revalidate(); } } } }); MainForm If the diff is larger than 0 then the title area should grow. We’re setting the preferred height to the diff plus the preferred height but we make sure not to cross the maximum height value. We then revalidate to refresh the UI
  • 59. } int diff = oldscrollY - scrollY; if(diff > 0) { if(titleArea.getHeight() < titleHeight) { titleArea.setPreferredH(Math.min(titleHeight, titleArea.getPreferredH() + diff)); titleArea.setHeight(titleArea.getPreferredH()); titleArea.getParent().revalidate(); } } else { if(diff < 0) { if(titleArea.getHeight() > 0) { titleArea.setPreferredH(Math.max(0, titleArea.getPreferredH() + diff)); titleArea.setHeight(titleArea.getPreferredH()); titleArea.getParent().revalidate(); } } } }); } } MainForm A negative diff is practically identical with the exception of making it 0 or larger instead of using the minimum value we use the max method. And with that the title folding is implemented
  • 60. } } else { if(diff < 0) { if(titleArea.getHeight() > 0) { titleArea.setPreferredH(Math.max(0, titleArea.getPreferredH() + diff)); titleArea.setHeight(titleArea.getPreferredH()); titleArea.getParent().revalidate(); } } } }); } } @Override protected void initGlobalToolbar() { Toolbar tb = new Toolbar(); tb.setTitleCentered(false); setToolbar(tb); } } MainForm The one last method in the class is this. We use a custom toolbar that disables centered title. The centered title places the title area in the center of the UI and it doesn’t work for folding. We need to disable it for this form so the title acts correctly on iOS.