Sunday, September 9, 2012

Android Layout. Dot the i's and cross the t's.

Most likely, you met Android Layouts during your first hours of android development. Android Layouts are primary and most important part of UI toolkit and it's very important to understand 'background' details and be aware about pitfalls to use them most efficiently.
I guess everyone know basic theory about layouts and how to use them, so this part will be ignored. The only thing need to noted is that as the basis of all layouts is ViewGroup, which holds other views (children) and defines LayoutParams - information used by views to tell their parents how they want to be laid out.
Now I propose to go directly to certain layout and it's implementation details.


    AbsoluteLayout

The main idea of AbsoluteLayout is to place each control at an absolute position. Here is source code of onLayout method which which performs children positioning:

@Override  
   protected void onLayout(boolean changed, int l, int t,  
       int r, int b) {  
     int count = getChildCount();  
     for (int i = 0; i < count; i++) {  
       View child = getChildAt(i);  
       if (child.getVisibility() != GONE) {  
         AbsoluteLayout.LayoutParams lp =  
             (AbsoluteLayout.LayoutParams) child.getLayoutParams();  
         int childLeft = mPaddingLeft + lp.x;  
         int childTop = mPaddingTop + lp.y;  
         child.layout(childLeft, childTop,  
             childLeft + child.getMeasuredWidth(),  
             childTop + child.getMeasuredHeight());  
       }  
     }  
   }  

It's quite obvious: every child is positioning on specified position (or left top point f layout if coordinates is not specified explicitly) considering position of parent layout and padding inside layout. Absolutely positioning makes your UI inflexible, so in most cases, it's not acceptable due to fragmentation of android devices.
AbsoluteLayout is simplest to use (and to implement), but due to significant restrictions it's very rarely used.

    FrameLayout

FrameLayout is used to display a single item at a time. You can have a few elements within a FrameLayout, but each element will be positioned based on the top left point of the screen. Elements, that overlaped by 'top' element, will be displayed overlapping. Child views are drawn in a stack, with the most recently added child on top, i.e. first view added to the frame layout will display on the bottom of the stack, and the last view added will display on top.
Frame layouts  usually used when you need to overlap views another views of your user interface. For example, you have application, which rendering some graphical data. In our case it will be newspaper. You have preview, which can be loaded immediately, while image with good quality requires some time to load and prepare. While second image is loading you can place spinner at the corner of preview to show, that 'load' operation in progress, something like this:

<?xml version="1.0" encoding="utf-8"?>  
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
        android:orientation="vertical"  
        android:layout_width="fill_parent"  
        android:layout_height="fill_parent"  
        android:padding="20dp"  
     >  
   <!--  
     UI elements...  
   -->  
   <ProgressBar  
       android:layout_width="wrap_content"  
       android:layout_height="wrap_content"  
       android:layout_gravity="right"  
   />  
 </FrameLayout>  

And how it will looks like:



After image with good quality is ready we can hide progress spinner and replace image in layout:


As we can see, frame layout is perfect choice for such situations. It allows us to overlap 'underlying' layer, but not block it, so user can start to interact with application.
So, now let's take a look on layout algorithm. At first step borders of FrameLayout are calculated:

@Override  
   protected void onLayout(boolean changed, int left, int top, int right, int bottom) {  
     final int count = getChildCount();  
     final int parentLeft = getPaddingLeftWithForeground();  
     final int parentRight = right - left - getPaddingRightWithForeground();  
     final int parentTop = getPaddingTopWithForeground();  
     final int parentBottom = bottom - top - getPaddingBottomWithForeground();  

Next, start loop through layout children to place them. For every non GONE element we will calculate left top point considering element's gravity:

for (int i = 0; i < count; i++) {  
       final View child = getChildAt(i);  
       if (child.getVisibility() != GONE) {  
         final LayoutParams lp = (LayoutParams) child.getLayoutParams();  
         ...

Left point is calculated according to horizontal gravity.

  final int layoutDirection = getResolvedLayoutDirection();  
     final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);  
     final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;  
     switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {  
        case Gravity.LEFT:  
           childLeft = parentLeft + lp.leftMargin;  
           break;  
        case Gravity.CENTER_HORIZONTAL:  
           childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +  
           lp.leftMargin - lp.rightMargin;  
           break;  
        case Gravity.RIGHT:  
           childLeft = parentRight - width - lp.rightMargin;  
           break;  
        default:  
           childLeft = parentLeft + lp.leftMargin;  
     }  

As we can see, by default, view is positioned to left corner (of course, margins included). Element width and parent borders are used if element must be positioned to the right corner or centered horizontally. Almost the same picture we have for top point of view (in this case default vertical gravity is TOP):

         switch (verticalGravity) {  
           case Gravity.TOP:  
             childTop = parentTop + lp.topMargin;  
             break;  
           case Gravity.CENTER_VERTICAL:  
             childTop = parentTop + (parentBottom - parentTop - height) / 2 +  
             lp.topMargin - lp.bottomMargin;  
             break;  
           case Gravity.BOTTOM:  
             childTop = parentBottom - height - lp.bottomMargin;  
             break;  
           default:  
             childTop = parentTop + lp.topMargin;  
         }  
         child.layout(childLeft, childTop, childLeft + width, childTop + height);  

and in the end, after we know coordinate of top left point of child view, will will call layout method to assign size and position to a view and all of its descendants.
So, FrameLayout is quite obvious and predictable. It's also really flexible, that's why it's used much often than AbsoluteLayout.

LinearLayout

The linear layout works as its name implies: it places elements in linear or vertical direction. When the layout’s orientation is set to vertical, all child elements within it are organized in a single column; when the layout’s orientation is set to horizontal, all child elements within it are placed in a single row. Due to its obviousness and simplicity LinearLayout is most commonly used layout manager. Unfortunately, linear layouts quite often used in situations, where another layout managers can solve this problem easily. LinearLayout may lead to a more complex and cumbersome layout description (your xml resources), which means creation of useless UI elements. Example will be considered below. 
Moment with size of elements within linear layout should be considered separately. Generally speaking, linear layout doesn't guarantee rubber layout. If elements have precise size (not one of FILL_PARENT(MATCH_PARENT), WRAP_CONTENT) it's possible situation, when their total size is greater than visible space of screen. Such cases breaks 'rubber layout' concept. But with linear layout we can avoid such situations, if we can find proportional coefficient (relative to the other elements) to replace hard coded size. Such technique in linear layout conception called weight. The weight of a child controls 'importance' of this element, i.e. how much  “space” its given within its parent linear layout. By the way, unlike other attributes of linear layout , which are applied to the linear layout view itself, this attribute applies to its child elements. 
The weight values should be numbers: max number for most prior item and min number for the lowest-priority element. You don't have to care about total size of values (I've read a few times, that sum of values must equals 1, but this is not true). You can specify weightSum for parent layout by yourself, but If  you will left it unspecified  - it will be computed at layout/measure time by adding the layout_weight of each child. And simple example:

 <?xml version="1.0" encoding="utf-8"?>  
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
        android:orientation="horizontal"  
        android:layout_width="fill_parent"  
        android:layout_height="fill_parent"  
        android:baselineAligned="false">  
   <LinearLayout android:background="#FF0000"  
          android:layout_height="fill_parent" android:layout_width="wrap_content"  
          android:layout_weight="1.2"/>  
   <LinearLayout android:background="#00FF00"  
          android:layout_height="fill_parent" android:layout_width="wrap_content"  
          android:layout_weight="0.3"/>  
   <LinearLayout android:background="#00FFFF"  
          android:layout_height="fill_parent" android:layout_width="wrap_content"  
          android:layout_weight="0.5"/>  
   <LinearLayout android:background="#FFFF00"  
          android:layout_height="fill_parent" android:layout_width="wrap_content"  
          android:layout_weight="1.5"/>  
 </LinearLayout>  

Due to our layout, yellow part should be the largest (about 43% of total width), red part is a little bit smaller, while cyan and green take together only approximately 23%. And on device it looks like in should be:


Now, lets take a look to ayout algorithm. It's is built around layout orientation: vertical or horizontal:

   @Override  
   protected void onLayout(boolean changed, int l, int t, int r, int b) {  
     if (mOrientation == VERTICAL) {  
       layoutVertical();  
     } else {  
       layoutHorizontal();  
     }  
   }  

Note first that layoutHorizontal and layoutVertical works in the same manner with respect to layout orientation. So, let's considering only one of them, layoutVertical. I won't include code of method here, it can be found here, we only briefly review the main steps. At first step border right coordinate is calculated. Then we get count of child in layout and calculate major and minor gravity:

     final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;  
     final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;  

Major gravity will be used on next step to calculate upper point from which items will be placed. Minor gravity will be applied to each child without explicitly specified gravity. In loop through layout children left point (where element should be placed) us calculated with respect to minor gravity (if there is no explicitly specified gravity for current element). And in the end, knowing left and top point where element should be placed and it's dimensions we can draw it. We should note, that some methods within loop have no any effects for linear layout and will be overridden for TableLayout (which based on LinearLayout), which will be considered next.
TableLayout is a grid of of rows and columns, where a cell can display a view control. From a user interface design perspective, a TableLayout is comprised of TableRow controls - one for each row in your table. The contents of a TableRow are simply the view controls that will go in each “cell” of the table grid.
Although table layouts can be used to design entire user interfaces, they usually aren’t the best solution for doing so, because they are derived from LinearLayout and not the most efficient of layout controls. Generally speaking, TableLayout is a little bit more than an organized set of nested LinearLayouts, and nesting layouts too deeply is generally discouraged for performance concerns. However, for data that is already in a format suitable for a table, such as spreadsheet data, table layout may be a reasonable choice.


RelativeLayout

Unlike linear or table layouts relative layout doesn't drive us to the limits of your current row - you can place elements where ever you want relatively to the other elements or even to parent layout. I guess this layout is very familiar to developers and I don't see any reason to focus on it. Just note, that considering capabilities of relative layout, its layout/measurement algorithm is much difficult. 
Let's consider common example of a layout: list item with an icon on the left, a title at the top and an optional description underneath the title:



This layout may be implemented using different layout, for example LinearLayout, RelativeLayout or TableLayout. Having the same result, we can compare layout's complexity by hierarchyviewer. So, here are layouts with different layout managers:

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
        android:layout_width="fill_parent"  
        android:layout_height="72dp"  
        android:padding="6dip">  
   <ImageView  
       android:id="@+id/icon"  
       android:layout_width="wrap_content"  
       android:layout_height="fill_parent"  
       android:layout_marginRight="6dip"  
       android:src="@drawable/ic_launcher" />  
   <LinearLayout  
       android:orientation="vertical"  
       android:layout_width="fill_parent"  
       android:layout_height="fill_parent">  
     <TextView  
         android:layout_width="fill_parent"  
         android:layout_height="0dip"  
         android:layout_weight="1"  
         android:gravity="center_vertical"  
         android:text="Layout Example item" />  
     <TextView  
         android:layout_width="fill_parent"  
         android:layout_height="0dip"  
         android:layout_weight="1"  
         android:singleLine="true"  
         android:ellipsize="marquee"  
         android:text="Simple item that shows how the same..." />  
   </LinearLayout>  
 </LinearLayout>  

 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"  
         android:layout_width="fill_parent"  
         android:layout_height="62dp"  
         android:padding="6dip">  
   <ImageView  
       android:id="@+id/icon"  
       android:layout_width="wrap_content"  
       android:layout_height="fill_parent"  
       android:layout_alignParentTop="true"  
       android:layout_alignParentBottom="true"  
       android:layout_marginRight="6dip"  
       android:src="@drawable/ic_launcher" />  
   <TextView  
       android:id="@+id/secondLine"  
       android:layout_width="fill_parent"  
       android:layout_height="26dip"  
       android:layout_toRightOf="@id/icon"  
       android:layout_alignParentBottom="true"  
       android:layout_alignParentRight="true"  
       android:singleLine="true"  
       android:ellipsize="marquee"  
       android:text="Simple item that shows how the same..." />  
   <TextView  
       android:layout_width="fill_parent"  
       android:layout_height="wrap_content"  
       android:layout_toRightOf="@id/icon"  
       android:layout_alignParentRight="true"  
       android:layout_alignParentTop="true"  
       android:layout_above="@id/secondLine"  
       android:layout_alignWithParentIfMissing="true"  
       android:gravity="center_vertical"  
       android:text="Layout Example item" />  
 </RelativeLayout>  

 <TableLayout xmlns:android="http://schemas.android.com/apk/res/android"  
        android:layout_width="fill_parent"  
        android:layout_height="72dp"  
        android:padding="6dip">  
   <TableRow  
       android:layout_height="wrap_content"  
       android:layout_width="match_parent">  
   <ImageView  
       android:id="@+id/icon"  
       android:layout_width="wrap_content"  
       android:layout_height="fill_parent"  
       android:layout_marginRight="6dip"  
       android:src="@drawable/ic_launcher"/>  
     <LinearLayout  
         android:orientation="vertical"  
         android:layout_width="fill_parent"  
         android:layout_height="fill_parent">  
       <TextView  
           android:layout_width="fill_parent"  
           android:layout_height="0dip"  
           android:layout_weight="1"  
           android:gravity="center_vertical"  
           android:text="Layout Example item"/>  
       <TextView  
           android:layout_width="fill_parent"  
           android:layout_height="0dip"  
           android:layout_weight="1"  
           android:text="Simple item that shows how the same..."/>  
     </LinearLayout>  
   </TableRow>  
 </TableLayout>  

And the difference between implementations becomes obvious when comparing the view hierarchies in HierarchyViewer:
Linear layout

Relative layout

Table layout

So, as we can see, we can create the same layout with different layout managers, but view hierarchies will be much different. For single item it may no sense to optimize view, but it much more important when you use such a layout for every item in a ListView, for instance. So I urge not to focus on single layout manager, but to choose it depending on certain problem.

No comments:

Post a Comment