Creating an Uber Clone - Part XIX - Transcript.pdf
1. Creating an Uber Clone - Part XIX
Now that the basic "infrastructure" is out of the way we'll start wiring it into the UI starting with search. The initial search UI I did in the mockup was very "naive". It just
toggled the completion on and off.
4. public class AutoCompleteAddressInput extends TextField {
private final Container layers;
private int firstX = -1, firstY = -1;
private boolean dragStarted;
private CompletionContainer completion;
private ActionListener<ActionEvent> dragListener, releaseListener;
private Location currentLocation;
private boolean blockChangeEvent;
public AutoCompleteAddressInput(String value, String hint,
Container layers, CompletionContainer completion) {
super(value, hint, 40, TextField.ANY);
this.completion = completion;
this.layers = layers;
getHintLabel().setUIID("FromToTextFieldHint");
setUIID("FromToTextField");
addDataChangedListener((i, ii) -> {
if(blockChangeEvent) {
return;
}
if(!getText().equals(value)) {
completion.updateCompletion(getText(), this);
}
AutoCompleteAddressInput
I refactored some of the code from the MapForm class into the AutoCompleteAddressInput class. It made it easier to implement some of the related logic.
I chose to derive TextField rather than encapsulate it mostly due to convenience. Encapsulation would have worked just as well for this case
5. public class AutoCompleteAddressInput extends TextField {
private final Container layers;
private int firstX = -1, firstY = -1;
private boolean dragStarted;
private CompletionContainer completion;
private ActionListener<ActionEvent> dragListener, releaseListener;
private Location currentLocation;
private boolean blockChangeEvent;
public AutoCompleteAddressInput(String value, String hint,
Container layers, CompletionContainer completion) {
super(value, hint, 40, TextField.ANY);
this.completion = completion;
this.layers = layers;
getHintLabel().setUIID("FromToTextFieldHint");
setUIID("FromToTextField");
addDataChangedListener((i, ii) -> {
if(blockChangeEvent) {
return;
}
if(!getText().equals(value)) {
completion.updateCompletion(getText(), this);
}
AutoCompleteAddressInput
With the exception of these last two variables every other variable here is in the service of the drag and drop logic
6. public class AutoCompleteAddressInput extends TextField {
private final Container layers;
private int firstX = -1, firstY = -1;
private boolean dragStarted;
private CompletionContainer completion;
private ActionListener<ActionEvent> dragListener, releaseListener;
private Location currentLocation;
private boolean blockChangeEvent;
public AutoCompleteAddressInput(String value, String hint,
Container layers, CompletionContainer completion) {
super(value, hint, 40, TextField.ANY);
this.completion = completion;
this.layers = layers;
getHintLabel().setUIID("FromToTextFieldHint");
setUIID("FromToTextField");
addDataChangedListener((i, ii) -> {
if(blockChangeEvent) {
return;
}
if(!getText().equals(value)) {
completion.updateCompletion(getText(), this);
}
AutoCompleteAddressInput
We use the DataChangedListener to send events to the completion logic, however this callback can be very verbose and it's sometimes invoked by setText. The solution
is a special version of `setText` that blocks this callback and reduces the noise in the completion code
with the blockChangeEvent variable
7. addDataChangedListener((i, ii) -> {
if(blockChangeEvent) {
return;
}
if(!getText().equals(value)) {
completion.updateCompletion(getText(), this);
}
});
}
public void setTextNoEvent(String text) {
blockChangeEvent = true;
setText(text);
blockChangeEvent = false;
}
@Override
protected void focusGained() {
completion.initCompletionBar();
}
@Override
protected void deinitialize() {
AutoCompleteAddressInput
The last focused text field is the one that now handles the completion so if the user was in the to text field everything typed will now impact the completion for to and visa
versa
8. protected void deinitialize() {
if(dragListener != null) {
Form f = getComponentForm();
f.removePointerDraggedListener(dragListener);
f.removePointerReleasedListener(dragListener);
}
super.deinitialize();
}
@Override
protected void initComponent() {
super.initComponent();
if(dragListener == null) {
dragListener = e -> {
Component cmp = layers.getComponentAt(1);
boolean dragUp = layers.getLayout().
getComponentConstraint(cmp).equals(SOUTH);
if(dragStarted) {
e.consume();
cmp.getUnselectedStyle().setMarginUnit(Style.UNIT_TYPE_PIXELS);
if(dragUp) {
cmp.setPreferredSize(new Dimension(getDisplayWidth(),
firstY - e.getY() + getDisplayHeight() / 8));
AutoCompleteAddressInput
Pointer listeners on the Form allow us to detect pointer events everywhere. We bind them in the initComponent method and remove them in the deinitialize method. This
prevents a memory leak and a situation where pointer processing code keeps running and taking up CPU. deinitialize is invoked when a component is removed from the
UI or its hierarchy is removed. It's also invoked when a different Form is shown instead of the current Form
9. protected void deinitialize() {
if(dragListener != null) {
Form f = getComponentForm();
f.removePointerDraggedListener(dragListener);
f.removePointerReleasedListener(dragListener);
}
super.deinitialize();
}
@Override
protected void initComponent() {
super.initComponent();
if(dragListener == null) {
dragListener = e -> {
Component cmp = layers.getComponentAt(1);
boolean dragUp = layers.getLayout().
getComponentConstraint(cmp).equals(SOUTH);
if(dragStarted) {
e.consume();
cmp.getUnselectedStyle().setMarginUnit(Style.UNIT_TYPE_PIXELS);
if(dragUp) {
cmp.setPreferredSize(new Dimension(getDisplayWidth(),
firstY - e.getY() + getDisplayHeight() / 8));
AutoCompleteAddressInput
initComponent is invoked when a component is "there". It will be invoked if a component is added to an already showing Form or if a parent Form is shown. You can rely
on initComponent and deinitialize working in tandem. They might be invoked multiple times in valid situations for instance a Dialog shown on top of a Form triggers a
deinitialize on the components of the Form followed by an initComponent when it's disposed
10. protected void deinitialize() {
if(dragListener != null) {
Form f = getComponentForm();
f.removePointerDraggedListener(dragListener);
f.removePointerReleasedListener(dragListener);
}
super.deinitialize();
}
@Override
protected void initComponent() {
super.initComponent();
if(dragListener == null) {
dragListener = e -> {
Component cmp = layers.getComponentAt(1);
boolean dragUp = layers.getLayout().
getComponentConstraint(cmp).equals(SOUTH);
if(dragStarted) {
e.consume();
cmp.getUnselectedStyle().setMarginUnit(Style.UNIT_TYPE_PIXELS);
if(dragUp) {
cmp.setPreferredSize(new Dimension(getDisplayWidth(),
firstY - e.getY() + getDisplayHeight() / 8));
AutoCompleteAddressInput
Despite using the shorthand lambda syntax for event handling I need to keep a reference to the drag and release event objects so I can remove them later
11. protected void deinitialize() {
if(dragListener != null) {
Form f = getComponentForm();
f.removePointerDraggedListener(dragListener);
f.removePointerReleasedListener(dragListener);
}
super.deinitialize();
}
@Override
protected void initComponent() {
super.initComponent();
if(dragListener == null) {
dragListener = e -> {
Component cmp = layers.getComponentAt(1);
boolean dragUp = layers.getLayout().
getComponentConstraint(cmp).equals(SOUTH);
if(dragStarted) {
e.consume();
cmp.getUnselectedStyle().setMarginUnit(Style.UNIT_TYPE_PIXELS);
if(dragUp) {
cmp.setPreferredSize(new Dimension(getDisplayWidth(),
firstY - e.getY() + getDisplayHeight() / 8));
AutoCompleteAddressInput
The dragged element is always the second element (0 is the first). It can be dragged between the CENTER location and the SOUTH location
12. protected void deinitialize() {
if(dragListener != null) {
Form f = getComponentForm();
f.removePointerDraggedListener(dragListener);
f.removePointerReleasedListener(dragListener);
}
super.deinitialize();
}
@Override
protected void initComponent() {
super.initComponent();
if(dragListener == null) {
dragListener = e -> {
Component cmp = layers.getComponentAt(1);
boolean dragUp = layers.getLayout().
getComponentConstraint(cmp).equals(SOUTH);
if(dragStarted) {
e.consume();
cmp.getUnselectedStyle().setMarginUnit(Style.UNIT_TYPE_PIXELS);
if(dragUp) {
cmp.setPreferredSize(new Dimension(getDisplayWidth(),
firstY - e.getY() + getDisplayHeight() / 8));
AutoCompleteAddressInput
If this is indeed a drag operation we'd like to block the event from propagating onwards
13. protected void deinitialize() {
if(dragListener != null) {
Form f = getComponentForm();
f.removePointerDraggedListener(dragListener);
f.removePointerReleasedListener(dragListener);
}
super.deinitialize();
}
@Override
protected void initComponent() {
super.initComponent();
if(dragListener == null) {
dragListener = e -> {
Component cmp = layers.getComponentAt(1);
boolean dragUp = layers.getLayout().
getComponentConstraint(cmp).equals(SOUTH);
if(dragStarted) {
e.consume();
cmp.getUnselectedStyle().setMarginUnit(Style.UNIT_TYPE_PIXELS);
if(dragUp) {
cmp.setPreferredSize(new Dimension(getDisplayWidth(),
firstY - e.getY() + getDisplayHeight() / 8));
AutoCompleteAddressInput
When a component is in the SOUTH we set its preferred size to one 8th of the display height so it won't peek up too much. When its dragged up we just increase that
size during drag
14. cmp.setPreferredSize(new Dimension(getDisplayWidth(),
firstY - e.getY() + getDisplayHeight() / 8));
} else {
cmp.getUnselectedStyle().setMarginTop(
Math.max(0, e.getY() - firstY));
}
layers.revalidate();
} else {
Component draggedCmp =
getComponentForm().getComponentAt(e.getX(), e.getY());
if(!draggedCmp.isChildOf((Container)cmp)) {
return;
}
if(firstX == -1) {
firstX = e.getX();
firstY = e.getY();
}
if((!dragUp && e.getY() - firstY > convertToPixels(2)) ||
(dragUp && firstY - e.getY() > convertToPixels(2))) {
e.consume();
dragStarted = true;
}
AutoCompleteAddressInput
Components in the CENTER ignore their preferred size and take up available space so we use margin to provide the drag effect
15. cmp.setPreferredSize(new Dimension(getDisplayWidth(),
firstY - e.getY() + getDisplayHeight() / 8));
} else {
cmp.getUnselectedStyle().setMarginTop(
Math.max(0, e.getY() - firstY));
}
layers.revalidate();
} else {
Component draggedCmp =
getComponentForm().getComponentAt(e.getX(), e.getY());
if(!draggedCmp.isChildOf((Container)cmp)) {
return;
}
if(firstX == -1) {
firstX = e.getX();
firstY = e.getY();
}
if((!dragUp && e.getY() - firstY > convertToPixels(2)) ||
(dragUp && firstY - e.getY() > convertToPixels(2))) {
e.consume();
dragStarted = true;
}
AutoCompleteAddressInput
This prevents a drag event on a different region in the form from triggering this event. E.g. if a user drags the map
16. (dragUp && firstY - e.getY() > convertToPixels(2))) {
e.consume();
dragStarted = true;
}
}
};
getComponentForm().addPointerDraggedListener(dragListener);
releaseListener = e -> {
if(dragStarted) {
e.consume();
Component cmp = layers.getComponentAt(1);
boolean dragUp = layers.getLayout().
getComponentConstraint(cmp).equals(SOUTH);
cmp.remove();
cmp.setUIID(cmp.getUIID());
boolean animateDown;
if(dragUp) {
animateDown = !(firstY - e.getY() > convertToPixels(8));
} else {
animateDown = e.getY() - firstY > convertToPixels(8);
}
if(animateDown) {
layers.add(SOUTH, cmp);
AutoCompleteAddressInput
Dragging just displayed a motion. We now need to remove the component and place it where it should be. We also reset the UIID so styling changes (e.g. margin, unit
type etc.) will reset to the default
17. if(dragUp) {
animateDown = !(firstY - e.getY() > convertToPixels(8));
} else {
animateDown = e.getY() - firstY > convertToPixels(8);
}
if(animateDown) {
layers.add(SOUTH, cmp);
cmp.setPreferredSize(
new Dimension(getDisplayWidth(), getDisplayHeight() / 8));
Style s = cmp.getUnselectedStyle();
s.setMarginUnit(Style.UNIT_TYPE_DIPS);
s.setMarginLeft(3);
s.setMarginRight(3);
} else {
layers.add(CENTER, cmp);
cmp.setPreferredSize(null);
}
layers.animateLayout(200);
firstX = -1;
firstY = -1;
dragStarted = false;
}
};
AutoCompleteAddressInput
When we place the container in the SOUTH we set the preferred size and margin to match. When we place it in the CENTER we set the preferred size to null which is a
special case that resets previous manual settings and restores the default
18. s.setMarginRight(3);
} else {
layers.add(CENTER, cmp);
cmp.setPreferredSize(null);
}
layers.animateLayout(200);
firstX = -1;
firstY = -1;
dragStarted = false;
}
};
getComponentForm().addPointerReleasedListener(releaseListener);
}
}
public Location getCurrentLocation() {
return currentLocation;
}
public void setCurrentLocation(Location currentLocation) {
this.currentLocation = currentLocation;
}
}
AutoCompleteAddressInput
The location of a text field uses strings but what we really care about is coordinates on the map which is why I store them here. This is used both by the map pin logic
and by the search logic we will use later on