SlideShare a Scribd company logo
1 of 60
Download to read offline
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

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

React new features and intro to Hooks
React new features and intro to HooksReact new features and intro to Hooks
React new features and intro to HooksSoluto
 
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docxCodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docxmary772
 
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docxCodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docxmccormicknadine86
 
Design pattern - Iterator, Mediator and Memento
Design pattern - Iterator, Mediator and MementoDesign pattern - Iterator, Mediator and Memento
Design pattern - Iterator, Mediator and MementoSean Tsai
 
Creating a Facebook Clone - Part XVI.pdf
Creating a Facebook Clone - Part XVI.pdfCreating a Facebook Clone - Part XVI.pdf
Creating a Facebook Clone - Part XVI.pdfShaiAlmog1
 
please code in c#- please note that im a complete beginner- northwind.docx
please code in c#- please note that im a complete beginner-  northwind.docxplease code in c#- please note that im a complete beginner-  northwind.docx
please code in c#- please note that im a complete beginner- northwind.docxAustinaGRPaigey
 
Creating a Facebook Clone - Part XXX - Transcript.pdf
Creating a Facebook Clone - Part XXX - Transcript.pdfCreating a Facebook Clone - Part XXX - Transcript.pdf
Creating a Facebook Clone - Part XXX - Transcript.pdfShaiAlmog1
 
[C++ gui programming with qt4] chap9
[C++ gui programming with qt4] chap9[C++ gui programming with qt4] chap9
[C++ gui programming with qt4] chap9Shih-Hsiang Lin
 
Creating a Whatsapp Clone - Part IV - Transcript.pdf
Creating a Whatsapp Clone - Part IV - Transcript.pdfCreating a Whatsapp Clone - Part IV - Transcript.pdf
Creating a Whatsapp Clone - Part IV - Transcript.pdfShaiAlmog1
 
Creating a Facebook Clone - Part XXIX - Transcript.pdf
Creating a Facebook Clone - Part XXIX - Transcript.pdfCreating a Facebook Clone - Part XXIX - Transcript.pdf
Creating a Facebook Clone - Part XXIX - Transcript.pdfShaiAlmog1
 
Extracting ui Design - part 6 - transcript.pdf
Extracting ui Design - part 6 - transcript.pdfExtracting ui Design - part 6 - transcript.pdf
Extracting ui Design - part 6 - transcript.pdfShaiAlmog1
 
Creating a Facebook Clone - Part XXVIII - Transcript.pdf
Creating a Facebook Clone - Part XXVIII - Transcript.pdfCreating a Facebook Clone - Part XXVIII - Transcript.pdf
Creating a Facebook Clone - Part XXVIII - Transcript.pdfShaiAlmog1
 
The War is Over, and JavaScript has won: Living Under the JS Regime
The War is Over, and JavaScript has won: Living Under the JS RegimeThe War is Over, and JavaScript has won: Living Under the JS Regime
The War is Over, and JavaScript has won: Living Under the JS Regimematthoneycutt
 
Adapting to Tablets and Desktops - Part 2.pdf
Adapting to Tablets and Desktops - Part 2.pdfAdapting to Tablets and Desktops - Part 2.pdf
Adapting to Tablets and Desktops - Part 2.pdfShaiAlmog1
 

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

React new features and intro to Hooks
React new features and intro to HooksReact new features and intro to Hooks
React new features and intro to Hooks
 
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docxCodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
 
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docxCodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
CodeZipButtonDemo.javaCodeZipButtonDemo.java Demonstrate a p.docx
 
Design pattern - Iterator, Mediator and Memento
Design pattern - Iterator, Mediator and MementoDesign pattern - Iterator, Mediator and Memento
Design pattern - Iterator, Mediator and Memento
 
Advance JFACE
Advance JFACEAdvance JFACE
Advance JFACE
 
Creating a Facebook Clone - Part XVI.pdf
Creating a Facebook Clone - Part XVI.pdfCreating a Facebook Clone - Part XVI.pdf
Creating a Facebook Clone - Part XVI.pdf
 
please code in c#- please note that im a complete beginner- northwind.docx
please code in c#- please note that im a complete beginner-  northwind.docxplease code in c#- please note that im a complete beginner-  northwind.docx
please code in c#- please note that im a complete beginner- northwind.docx
 
Creating a Facebook Clone - Part XXX - Transcript.pdf
Creating a Facebook Clone - Part XXX - Transcript.pdfCreating a Facebook Clone - Part XXX - Transcript.pdf
Creating a Facebook Clone - Part XXX - Transcript.pdf
 
Bot builder v4 HOL
Bot builder v4 HOLBot builder v4 HOL
Bot builder v4 HOL
 
[C++ gui programming with qt4] chap9
[C++ gui programming with qt4] chap9[C++ gui programming with qt4] chap9
[C++ gui programming with qt4] chap9
 
GWT Widgets
GWT WidgetsGWT Widgets
GWT Widgets
 
Creating a Whatsapp Clone - Part IV - Transcript.pdf
Creating a Whatsapp Clone - Part IV - Transcript.pdfCreating a Whatsapp Clone - Part IV - Transcript.pdf
Creating a Whatsapp Clone - Part IV - Transcript.pdf
 
Creating a Facebook Clone - Part XXIX - Transcript.pdf
Creating a Facebook Clone - Part XXIX - Transcript.pdfCreating a Facebook Clone - Part XXIX - Transcript.pdf
Creating a Facebook Clone - Part XXIX - Transcript.pdf
 
Extracting ui Design - part 6 - transcript.pdf
Extracting ui Design - part 6 - transcript.pdfExtracting ui Design - part 6 - transcript.pdf
Extracting ui Design - part 6 - transcript.pdf
 
Windows Phone Launchers and Choosers
Windows Phone Launchers and ChoosersWindows Phone Launchers and Choosers
Windows Phone Launchers and Choosers
 
Creating a Facebook Clone - Part XXVIII - Transcript.pdf
Creating a Facebook Clone - Part XXVIII - Transcript.pdfCreating a Facebook Clone - Part XXVIII - Transcript.pdf
Creating a Facebook Clone - Part XXVIII - Transcript.pdf
 
JavaScript Refactoring
JavaScript RefactoringJavaScript Refactoring
JavaScript Refactoring
 
Final_Project
Final_ProjectFinal_Project
Final_Project
 
The War is Over, and JavaScript has won: Living Under the JS Regime
The War is Over, and JavaScript has won: Living Under the JS RegimeThe War is Over, and JavaScript has won: Living Under the JS Regime
The War is Over, and JavaScript has won: Living Under the JS Regime
 
Adapting to Tablets and Desktops - Part 2.pdf
Adapting to Tablets and Desktops - Part 2.pdfAdapting to Tablets and Desktops - Part 2.pdf
Adapting to Tablets and Desktops - Part 2.pdf
 

More from ShaiAlmog1

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

More from ShaiAlmog1 (20)

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

Recently uploaded

A Domino Admins Adventures (Engage 2024)
A Domino Admins Adventures (Engage 2024)A Domino Admins Adventures (Engage 2024)
A Domino Admins Adventures (Engage 2024)Gabriella Davis
 
Benefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other FrameworksBenefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other FrameworksSoftradix Technologies
 
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Patryk Bandurski
 
Azure Monitor & Application Insight to monitor Infrastructure & Application
Azure Monitor & Application Insight to monitor Infrastructure & ApplicationAzure Monitor & Application Insight to monitor Infrastructure & Application
Azure Monitor & Application Insight to monitor Infrastructure & ApplicationAndikSusilo4
 
Slack Application Development 101 Slides
Slack Application Development 101 SlidesSlack Application Development 101 Slides
Slack Application Development 101 Slidespraypatel2
 
Scaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organizationScaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organizationRadu Cotescu
 
IAC 2024 - IA Fast Track to Search Focused AI Solutions
IAC 2024 - IA Fast Track to Search Focused AI SolutionsIAC 2024 - IA Fast Track to Search Focused AI Solutions
IAC 2024 - IA Fast Track to Search Focused AI SolutionsEnterprise Knowledge
 
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024BookNet Canada
 
Unblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen FramesUnblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen FramesSinan KOZAK
 
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | DelhiFULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhisoniya singh
 
Breaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path MountBreaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path MountPuma Security, LLC
 
Salesforce Community Group Quito, Salesforce 101
Salesforce Community Group Quito, Salesforce 101Salesforce Community Group Quito, Salesforce 101
Salesforce Community Group Quito, Salesforce 101Paola De la Torre
 
Factors to Consider When Choosing Accounts Payable Services Providers.pptx
Factors to Consider When Choosing Accounts Payable Services Providers.pptxFactors to Consider When Choosing Accounts Payable Services Providers.pptx
Factors to Consider When Choosing Accounts Payable Services Providers.pptxKatpro Technologies
 
The Codex of Business Writing Software for Real-World Solutions 2.pptx
The Codex of Business Writing Software for Real-World Solutions 2.pptxThe Codex of Business Writing Software for Real-World Solutions 2.pptx
The Codex of Business Writing Software for Real-World Solutions 2.pptxMalak Abu Hammad
 
[2024]Digital Global Overview Report 2024 Meltwater.pdf
[2024]Digital Global Overview Report 2024 Meltwater.pdf[2024]Digital Global Overview Report 2024 Meltwater.pdf
[2024]Digital Global Overview Report 2024 Meltwater.pdfhans926745
 
GenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationGenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationMichael W. Hawkins
 
My Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationMy Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationRidwan Fadjar
 
Pigging Solutions Piggable Sweeping Elbows
Pigging Solutions Piggable Sweeping ElbowsPigging Solutions Piggable Sweeping Elbows
Pigging Solutions Piggable Sweeping ElbowsPigging Solutions
 
Key Features Of Token Development (1).pptx
Key  Features Of Token  Development (1).pptxKey  Features Of Token  Development (1).pptx
Key Features Of Token Development (1).pptxLBM Solutions
 
SIEMENS: RAPUNZEL – A Tale About Knowledge Graph
SIEMENS: RAPUNZEL – A Tale About Knowledge GraphSIEMENS: RAPUNZEL – A Tale About Knowledge Graph
SIEMENS: RAPUNZEL – A Tale About Knowledge GraphNeo4j
 

Recently uploaded (20)

A Domino Admins Adventures (Engage 2024)
A Domino Admins Adventures (Engage 2024)A Domino Admins Adventures (Engage 2024)
A Domino Admins Adventures (Engage 2024)
 
Benefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other FrameworksBenefits Of Flutter Compared To Other Frameworks
Benefits Of Flutter Compared To Other Frameworks
 
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
 
Azure Monitor & Application Insight to monitor Infrastructure & Application
Azure Monitor & Application Insight to monitor Infrastructure & ApplicationAzure Monitor & Application Insight to monitor Infrastructure & Application
Azure Monitor & Application Insight to monitor Infrastructure & Application
 
Slack Application Development 101 Slides
Slack Application Development 101 SlidesSlack Application Development 101 Slides
Slack Application Development 101 Slides
 
Scaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organizationScaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organization
 
IAC 2024 - IA Fast Track to Search Focused AI Solutions
IAC 2024 - IA Fast Track to Search Focused AI SolutionsIAC 2024 - IA Fast Track to Search Focused AI Solutions
IAC 2024 - IA Fast Track to Search Focused AI Solutions
 
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
Transcript: #StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
 
Unblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen FramesUnblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen Frames
 
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | DelhiFULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
 
Breaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path MountBreaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path Mount
 
Salesforce Community Group Quito, Salesforce 101
Salesforce Community Group Quito, Salesforce 101Salesforce Community Group Quito, Salesforce 101
Salesforce Community Group Quito, Salesforce 101
 
Factors to Consider When Choosing Accounts Payable Services Providers.pptx
Factors to Consider When Choosing Accounts Payable Services Providers.pptxFactors to Consider When Choosing Accounts Payable Services Providers.pptx
Factors to Consider When Choosing Accounts Payable Services Providers.pptx
 
The Codex of Business Writing Software for Real-World Solutions 2.pptx
The Codex of Business Writing Software for Real-World Solutions 2.pptxThe Codex of Business Writing Software for Real-World Solutions 2.pptx
The Codex of Business Writing Software for Real-World Solutions 2.pptx
 
[2024]Digital Global Overview Report 2024 Meltwater.pdf
[2024]Digital Global Overview Report 2024 Meltwater.pdf[2024]Digital Global Overview Report 2024 Meltwater.pdf
[2024]Digital Global Overview Report 2024 Meltwater.pdf
 
GenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationGenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day Presentation
 
My Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationMy Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 Presentation
 
Pigging Solutions Piggable Sweeping Elbows
Pigging Solutions Piggable Sweeping ElbowsPigging Solutions Piggable Sweeping Elbows
Pigging Solutions Piggable Sweeping Elbows
 
Key Features Of Token Development (1).pptx
Key  Features Of Token  Development (1).pptxKey  Features Of Token  Development (1).pptx
Key Features Of Token Development (1).pptx
 
SIEMENS: RAPUNZEL – A Tale About Knowledge Graph
SIEMENS: RAPUNZEL – A Tale About Knowledge GraphSIEMENS: RAPUNZEL – A Tale About Knowledge Graph
SIEMENS: RAPUNZEL – A Tale About Knowledge Graph
 

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.