Meet the Green Goblin, part 1
Last Friday we announced a significant update to the Android Market client. A whole slew of features went into this update (and many more are to come), and this week the pixel geek in me will be talking about the new visual design of the application.
To kick off this series, let’s talk about the layouts. Traditionally, the application design breaks the screens into separate sections – headers with basic navigation controls, main content area and perhaps the footer with context-sensitive action controls. This works especially well when each section occupies a rectangular portion of the screen – with the layout managers and the painting pipeline optimized for non-overlapping rectangular views. If you follow the recent visual trends in mobile, desktop and web design, you would notice that the designers are starting to push the bounds of the underlying UI toolkits to explore richer layouts with overlapping free-form controls. A few weeks ago our designers have come up with a new approach for browsing the main market content and a richer interaction model that puts more control into the hands of the users.
As noted in the announcement, we’re using an interactive carousel that enables a much more immersive interaction with the content in both portrait and landscape mode. In portrait mode, the list of featured applications “disappears” below the carousel, while in landscape mode the list is placed to the right of the carousel. Once the list starts scrolling, it goes below the carousel – with the row contents still partially visible in the left and right “ears”. If you were to open this activity in the hierarchy viewer, you would see two overlapping top-level views:
The carousel and the list overlap in the area that “contains” the curved bottom edge of the carousel. Without this overlap the topmost visible row in the list would be clipped at the bottom-most point of the arc with very noticeable white areas in the left and right ears of the carousel. While the core layout classes support a wide variety of cases, you’ll need to write a custom layout in case you have overlapping child views. Here is what you need to keep in mind:
- You can extend an existing core class, such as LinearLayout. It depends on how much tweaking you need to do after the base class does its magic.
- The LayoutParams defined in ViewGroup class supports a very rudimentary set of attributes (width and height). You can extend the LayoutParams class and add your own attributes (in much the same way the LinearLayout does, for instance). Add an attribute definition in your res/attrs.xml, enumerating all possible values and a declare-styleable entity with your custom atribute(s). Then, define a Java class with static ints that correspond to the attribute values. Then, extend one of the core LayoutParams classes (depends what exactly you need from the core) and get the values for your custom attributes. Finally, override the generateLayoutParams method in your container to return an instance of your layout params class. When you need the actual attribute value at runtime, cast the result of getLayoutParams of the specific child view to your custom class and you’re good to go.
- In your custom layout, implement both onMeasure and onLayout. Make sure that the final width and height that you set in child.layout inĀ onLayout match the measured width and height that you pass to child.measure in onMeasure. In case you see incorrect bounds of grand-child views, open the hierarchy viewer and compare the actual and measured values for width / height.
- If child views overlap, you most probably want to have some kind of transparency or translucency in the overlapping area. The child views are painted in the order that is defined in the layout XML. In our case, we want the list view to be drawn before the carousel – otherwise the topmost visible row will be painted on top of the bottom curved edge of the carousel. This means that you will have some degree of coupling between the XML layout definition and the measure / layout code.
Let’s take another look at the overlapping area. As you can see, the list has a “Featured” header row. This is a simple text view with a light gray background. However, there is one very important usability setting on this header view – the top padding. As noted before, the top edge of the list is above the bottom-most point of the curved arc. If we don’t set any top padding on this list header, its text will be partially obscured by the carousel curve resulting in a very poor UI experience. The question is how do we compute the top padding?
The actual value depends on the pixel size of the screen. Remember that there’s a whole bunch of different devices out there, and you cannot make any assumptions about the screen hardware – physical size, pixel size or density. This is directly related to our curved carousel – the curvature radius and, by extension, the vertical size of the overlap area are only known at runtime.
If we were to define the overlap area height to be, say, 30 units (density-independent pixels), the arc would have different perceived curvature across devices such as G1, Nexus and Samsung Tab. If you target a “common” hardware such as Nexus / Droid / EVO, you will end up with an arc that looks too steep on a G1 and too shallow on a Tab. So, instead of hard-coding the overlap area height – and by extension the curvature radius – at design time, we compute it at runtime as the function of the actual pixel size of the screen.
This overlap area height is then used for two purposes. First, it defines the measured and actual height of the carousel and list view. If you were to add the two heights and subtract the overlap height, you would end up with the exact height of the screen. Second, we set it as the top padding on the list header view. This effectively pushes down the label so that it is fully visible (remember that you cannot rely that the text is short if your app is localized), with an extra bonus of extending the light gray background all the way to the carousel arc ears.
Up until now we talked about the portrait mode. What about the landscape?
Here, the design places the carousel and the list side by side in order to better reuse the available screen estate (more on this in tomorrow’s episode). All of the points mentioned for the portrait are relevant here, modulo the following:
- The arc curvature and overlap amount are based on the screen width.
- The overlap is along the X axis, with both views spanning the full height of the parent.
- Each list row has a custom left padding that is based on the overlap area width. This prevents the app icons from being partially obscured by the right curved arc.
- The same padding is set on the list header view.
- If we were to set the padding on the list itself, we wouldn’t be able to have the header view background and list separators extending all the way to the left and below the carousel. The UI would be usable, but uglier.
This is it for today. The next entry talks about the landscape orientation and how the design aims to use the available screen estate without cramming too many visual elements.