Custom Layouts on Android
If you ever built an Android app, you have definitely used some of the built-in layouts available in the platform—RelativeLayout, LinearLayout, FrameLayout, etc. They are our bread and butter for building Android UIs.
The built-in layouts combined provide a powerful toolbox for implementing complex UIs. But there will still be cases where the design of your app will require you to implement custom layouts.
There are two main reasons to go custom. First, to make your UI more efficient—by reducing the number of views and/or making faster layout traversals. Second, when you are building UIs that are not naturally possible to implement with stock widgets.
In this post, I will demonstrate four different ways of implementing custom layouts and discuss their respective pros and cons: composite view, custom composite view, flat custom view, and async custom views.
The code samples are available in my android-layout-samples repo. This app implements the same UI with each technique discussed here and uses Picasso for image loading. The UI is a simplified version of Twitter app’s stream—no interactions, just the layouts.
Ok, let’s start with the most common type of custom layout: composite view.
This is usually your starting point. Composite views (also known as compound views) are the easiest way of combining multiple views into a reusable UI component. They are very simple to implement:
- Subclass one of the built-in layouts.
- Inflate a merge layout in the constructor.
- Initialize members to point to inner views with findViewById().
- Add your own APIs to query and update the view state.
TweetCompositeViewcode is a composite view. It subclasses RelativeLayout, inflates tweet_composite_layout.xmlcode into it, and exposes an update() method to refresh its state in the adaptercode. Simple stuff.
Custom Composite View
TweetCompositeView will likely perform fairly well in most situations. But, for sake of argument, suppose you want to reduce the number of child views and make layout traversals a bit more efficient.
Although composite views are simple to implement, using general-purpose layouts has a cost—especially with complex containers like LinearLayout and RelativeLayout. As platform building blocks, they have to handle tons of layout combinations and might have to measure child views multiple times in a single traversal—LinearLayout‘s layout_weight being a common example.
You can greatly optimize your UI by implementing a custom logic for measuring and positioning child views that is specially tailored for your app. This is what I like to call a custom composite view.
A custom composite view is simply a composite view that overrides onMeasure() and onLayout(). So, instead of subclassing an existing container like RelativeLayout, you will be subclassing the more general ViewGroup.
TweetLayoutViewcode implements this technique. Note how it gets rid of the LinearLayout from TweetComposiveView and avoids the use of layout_weight altogethercode.
The laborious work of figuring out what MeasureSpec to use on each child view is done by the ViewGroup’s handy measureChildWithMargins() method—and getChildMeasureSpec() under the hood.
TweetLayoutView probably doesn’t handle all possible layout combinations correctly but it doesn’t have to. It is absolutely fine to optimize custom layouts for your specific needs. This allows you to write both simpler and more efficient layout code for your app.
Flat Custom View
As you can see, custom composite views are fairly simple to write using ViewGroup APIs. Most of the time, they will give you the performance your app needs.
However, you might want to go further and optimize your layouts even more on critical parts of your UI that are very dynamic e.g. ListViews, ViewPager, etc. What about merging all TweetLayoutView child views into a single custom view to rule them all? That is what flat custom views are about—see image below.
Custom Composite View (left) and Flat Custom View (right)
A flat custom view is a fully custom view that measures, arranges, and draws its inner elements. So you will be subclassing View instead of ViewGroup.
If you are looking for real-world examples, enable the “show layout bounds” developer option in your device and have a look at apps like Twitter, GMail, and Pocket. They all use flat custom views in their listing UIs.
The main benefit from using flat custom views is the great potential for flattening your view hierarchy, resulting in faster traversals and, potentially, a reduced memory footprint.
Flat custom views give you maximum freedom as they are literally a blank canvas. But this comes at a price: you won’t be able to use the feature-packed stock widgets such as TextView and ImageView. Yes, it is simple to draw text on a Canvas but what about ellipsizing? Yes, you can easily draw a bitmap but what about scaling modes? Same applies to touch events, accessibility, keyboard navigation, etc.
The bottom line is: with flat custom views,you will likely have to re-implement features that you would get for free from the platform. So you should only consider using them on core parts of your UI. For all other cases, just lean on the platform with composite views, custom or not.
TweetElementViewcode is a flat custom view. To make it easier to implement it, I created a little custom view framework called UIElement. You will find it in the canvascode package.
The UIElement framework provides a measure/layout API which is analogous to Android’s. It contains headless versions of TextView and ImageView with only the necessary features for this demo—see TextElementcode and ImageElementcode respectively. It also has its own inflatercode to instantiate UIElements from layout resource filescode.
Probably worth noting: the UIElement framework is in a very early development stage. Consider it a very rough sketch of something that might actually become useful in the future.
You have probably noticed how simple TweetElementView looks. This is because the real code is all in TweetElementcode—with TweetElementView just acting as a hostcode.
The layout code in TweetElement is pretty much analogous to TweetLayoutView‘s. It handles Picasso requests differentlycode because it doesn’t use ImageViews.
Async Custom View
As we all know, the Android UI framework is single-threaded. And this is for a good reason: UI toolkits are not your simplest piece of code. Making them thread-safe and asynchronous would be an unworthy herculean effort.
This single-threaded nature leads to some fundamental limitations. It means, for example, that you can’t do layout traversals off main thread at all—something that would be useful in very complex and dynamic UIs.
For example, if your app has complex items in a ListView (like most social apps do), you will probably end up skipping frames while scrolling because ListView has to measurecode and layoutcode each child view to account for their new content as they become visible. The same issue applies to GridViews, ViewPagers, and the like.
Wouldn’t it be nice if we could do a layout traversal on the child views that are not visible yet without blocking the main thread? This way, the measure() and layout() calls on child views would take no time when needed in the UI thread.
Enter async custom view, an experiment to allow layout passes to happen off main thread. This is inspired by the async node framework developed by the Paper team at Facebook.
Given that we can never ever touch the UI toolkit off main thread, we need an API that is able to measure/layout the contents of a view without being directly coupled to it. This is exactly what the UIElement framework provides us.
AsyncTweetViewcode is an async custom view. It uses a thread-safe AsyncTweetElementcode factorycode to define its contents. Off-screen AsyncTweetElement instances are created, pre-measured, and cached in memory from a background thread using a Smoothie item loadercode.
I had to compromise the async behaviour a bit because there’s no sane way of showing layout placeholders on list items with arbitrary heights i.e. you end up resizing them once the layout gets delivered asynchronously. So whenever an AsyncTweetView is about to be displayed and it doesn’t find a matching AsyncTweetElement in memory, it will force its creation in the UI threadcode.
Furthermore, both the preloading logic and the memory cache expiration would need to be a lot smarter to ensure more layout cache hits in the UI thread. For instance, using a LRU cachecode here doesn’t seem ideal.
Despite these limitations, the preliminary results from async custom views look very promising. I’ll continue the explorations in this area by refining the UIElement framework and using it in other kinds of UIs. Let’s see where it goes.
When it comes to layouts, the more custom you go, the less you’ll be able to lean on the platform’s proven components. So avoid premature optimization and only go fully custom on areas that will actually affect the perceived quality and performance of your app.
This is not a black-and-white decision though. Between stock widgets and fully custom views there’s a wide spectrum of solutions—from simple composite views to the more complex async views. In practice, you’ll usually end up combining more than one of the techniques demonstrated here.
Syndicated 2014-05-12 12:42:06 from Lucas Rocha