Introduction

When it comes to add more and more features, things can get really messy and it is easy to be in a situation where your users are going to suffer because the main functionality is hidden by a dozen of secondary ones. It becomes also pretty much impossible to evolve a tool in order to meet deadlines and new requirements. Last year, in Hailo we decided to do a step back and align designers with developers in order to refactor our application with the intent to simplify the overall User Experience, trying at the same time to keep the UI clean and extensible as much as possible.

Legacy Hailo App

Current Version 4.15

In this post we are not going to focus our attention on each screen change in the app. It would be definitely impossible to summarize one year of work in just a blog post and we’ll rather show how and why some of the Custom components have contributed, and still contribute, to the evolution of our Android product.

Concepts

Before getting to a real example, we are going to list and explain some of the concepts, contracts and reasons why those have been taken into considerations when developing the new UI components.

General (Android)

  • A View hierarchy is represented as a tree data-structure made of Views
    • operations tend to become more expensive as soon as the tree becomes like a list (O(N))
  • A layout is a two pass process and it’s generally expensive(depending on the number of views and how they are positioned)
    • onMeasure (top-down): allows to specify the size of the view based on parent constraints
    • onLayout (top-down): used to position and set final dimensions after onMeasure
  • Complex View hierarchies usually result in poor rendering performances
  • Overdrawing pixels steals frame time for unnecessary operations
  • Allocations when performing continuous iterations(touch events, drawing, ...) can fire the Garbage Collector and this often increases the number of frames dropped when doing screen rendering

Hailo

Since the beginning of our “New UI Journey”, one of the main points taken in consideration when developing new components has been having a UI and UX that look simple to the user and don’t hide the main application functionality by adding new features. This gets even more complicated when components have to be pluggable and sometimes connected to experiments running on the server side but,at the same time, it gives us the capability to test the real benefit of a new solution in comparison with an existing one.

To achieve this result sometimes you have to abandon the simple way of doing things and keep in mind there might be a better solution. In the case of UI, Custom Views have allowed us to express complex concepts through a simpler but non standard representation.

Building an adaptable custom view

In Hailo application, the simplest example can be definitely the allocation screen that is shown as soon as the customer confirms a job order and tries to hail a standard ride.

Before creating our Custom View by extending the View class, we have to declare the custom attributes that will be used in order to provide ad-hoc functionalities. These attributes will later allow us to provide different customizations.

<declare-styleable name="AllocationBar">
    <attr name="arcColor" format="color"/>
    <attr name="arcStartAngle" format="float"/>
    <attr name="externalArcStrokeWidth" format="dimension"/>
    <attr name="internalArcStrokeWidth" format="dimension"/>
    <attr name="circleBitmapDrawable" format="reference"/>
    <attr name="rippleColor" format="color"/>
</declare-styleable>

We now have to support the above created attributes in the custom view before being able to use them in an XML layout.

public class AllocationBar extends View {

    public AllocationBar(Context ctx, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        final TypedArray a = ctx.obtainStyledAttributes(attrs, R.styleable.AllocationBar, ...);

        // Trying to get the style attributes
        mArcColor = a.getColor(R.styleable.AllocationBar_arcColor, DEFAULT_ARC_COLOR);
        
        // Finally recycling the TypedArray
        a.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // The AllocationBar will be a square with an edge made     
        // by the shortest dimension between the provided(available) height and width
        
        // We MUST then specify the dimensions of this view
        setMeasuredDimension(measuredEdge, measuredEdge);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        // In onLayout we can finally use the dimension to set up our view
        // and all the elements used to draw our UI
        
    }

    @Override
    protected void onDraw(Canvas canvas) {
        
        // Drawing the external arc if visible
        if (mExtScale > 0f) {
            canvas.save();
            canvas.rotate(mExtRotate, mX, mY);
            canvas.scale(mExtScale, mExtScale, mX, mY);
            canvas.drawArc(mExternalArc, 0f, ARC_ANGLE, false, mArcPaint);
            canvas.restore();
        }
        
    }
}

After having a look at the code above you should be able to understand what are the main steps when implementing a fully Custom View. You should always strive to keep in mind the contracts/suggestions/rules presented in the concepts sections and try to maintain the logic as simple and smart as possible.

As a developer, one of the main benefits of having a widget like AllocationBar that supports custom attributes is the possibility to have a UI preview directly in the IDE without having to deploy the entire application on the device/emulator everytime we change a value to the device. Even if currently there are some limitations(e.g.: you cannot animate a view), this can save us a lot of time when developing.

The missing part in the AllocationBar is the one related to animating the indeterminate progress. We have seen in the previous GIF two arcs moving all around the central bitmap. At the same time, starting and fading out from the middle, there are three ripples giving a kind of “drop in the water” effect and generating some waves. All of this can be achieved implementing animators that allow us with a simple effort to update the current UI state, notifying when done the AllocationBar through an invalidate call. The animation above is fairly complex but if we have a look at the external arc we can decompose its path in 3 different nodes which can be played sequentially by an AnimatorSet:

  1. Scale-In while rotating Clockwise (Accelerate-Decelerate interpolator) - 400ms
  2. Rotate Anti-Clockwise (Decelerate-Accelerate interpolator [custom]) - 1000ms
  3. Linear Clockwise rotation (Linear interpolator) - infinite - 1200ms
ValueAnimator animator = ValueAnimator.ofPropertyValuesHolder(...);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
    @Override
    public void onAnimationUpdate(ValueAnimator animator) {
        mExtRotate = (Float)animator.getAnimatedValue(ROTATE);
        mExtScale = (Float)animator.getAnimatedValue(SCALE); // only for 1)
        // N.B.: invalidate expects int values not float ones
        //  here it’s just a tutorial and we want to make it readable
        invalidate(ripple.left, ripple.top, ripple.right, ripple.bottom);
    }
});
animator.setInterpolator(...);

Thinking about the future and new technologies, can you imagine now how to port the allocation view to a wearable device? Currently, Hailo is not available on AndroidWear but our users are already used to the allocation UI pattern when waiting for a cab. Therefore, they probably expect to see the same approach and style on a wearable device. Having a custom view designed to be scaled across multiple screens with its own set of customizable attributes that encapsulates all UI logic allows us to share the implementation and just declare it in the wearable layout.

<?xml version="1.0" encoding="utf-8"?>
    <com.hailo.widgets.AllocationBar xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/allocation_bar"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:background="#000000"
    app:arcColor="#FFE34C"
    app:arcStartAngle="120"
    app:circleBitmapDrawable="@drawable/ic_taxi_circular"
    app:externalArcStrokeWidth="10dp"
    app:internalArcStrokeWidth="8dp"
    app:rippleColor="#FFE34C" />

In the case of a wearable device, our custom view is already optimized to work on both square and round screens. Therefore, we can avoid to embed it in a BoxInsetLayout saving us from adding another level in the view hierarchy. Considering the fact that a wearable device has generally limited resources, we still want to decorate our custom view by adding a new attribute that can turn on/off the ripples or that can provide a kind of light-mode behaviour. Even if this might look as an unnecessary optimization, in the context where a device can enter low-powered Ambient mode to save resources, we have to think about the impact of displaying something too heavy and provide a solution that is not just looking good visually in one case. Doing this allows the device to save precious resources by limiting the number of pixels being updated on screen and creating an overall better platform experience.

Final thoughts

Beside the performance boost gained in some cases after having the chance of flattening the View hierarchy and also reducing the Overdraw level, building Custom Views has simplified the work done to create modular UI components which are not tightly coupled with a special function and/or place in the app. Of course, there is an initial cost of developing them which requires a deeper knowledge about how Views work behind the scenes but, at the same time, the result encapsulates better the UI logic becoming more maintainable and possibly agnostic to the context where it is used.