Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Building Dynamic UI for Android* Devices

2 Jun 2014 1  
In this document, we will focus on a few Android UI programming techniques that will help you to achieve the goal of a dynamic UI – use of the action bar, tab, and swipe views with dynamic application data to enhance the screen navigation, use of Android fragments to design multiple-pane and master-

This article is in the Product Showcase section for our sponsors at CodeProject. These articles are intended to provide you with information on products and services that we consider useful and of value to developers.

Abstract

What does "Dynamic UI" mean for Android* developers? As an Android app developer, you want the UI of your application to adapt to the dynamic nature of the application content. You also want the UI of your application to fit and flow well on the majority of Android devices, regardless of the size of the display, the screen resolution, or the pixel density. Given the variety of Android devices on the market and the challenge of keeping up with the latest Android SDK, this can be a difficult task.

In this document, we will focus on a few Android UI programming techniques that will help you to achieve the goal of a dynamic UI – use of the action bar, tab, and swipe views with dynamic application data to enhance the screen navigation, use of Android fragments to design multiple-pane and master-view layout for screens with different sizes of display, and use of the Android resource system to improve the presentation of graphics and text content for screens with different resolution and density.

Table of Contents

  1. Introduction
  2. Sample restaurant menu application
  3. Action bar, tab, swipe view, and dynamic application data
  4. Android UI fragment, multi-pane layout, and master-detail view
  5. Android resource system, graphics and text, screen resolution and density
  6. Conclusion
  7. References

Introduction

This document discusses dynamic UI building from 3 aspects:

  1. Displaying dynamic content with the latest Android UI pattern and controls – At minimum, you want the UI of your application to "live" and "breathe" like the rest of the Android systems. One key is to design your app with the latest Android UI components, controls, and navigation patterns. A certain degree of customization is not a problem, but in general, you want to adhere to the latest Android guidelines and trends. And, how do we present the dynamic application content with this latest UI trend? In this paper, we will demonstrate the use of the latest Android UI components, such as the action bar, tab, and swipe view, to present dynamic application data.
  2. UI layout and navigation flow – Does it make sense to have the same UI layout for both 4" phone and 11" tablet? To maximize the space from the large display, sometimes you want to consider using different layouts for devices with different sizes of display. In this document, we will discuss the use of Android Fragment to design multi-pane/single-pane layouts and master-detail view to fit screens with different display sizes.
  3. Display resolution and density – Besides the UI layout, if you use graphics in your app, what can you do to ensure the graphics are not stretched or pixelated on devices with different screen resolutions and pixel densities? What about the font size of a text item? A text item with a font size of 20 may look perfect on a phone, but it could be too small on a tablet. We will discuss what you can do with the Android resource system to handle these issues.

Sample Restaurant Menu App

To illustrate the programming concepts described in this document, I wrote an Android app that allows users to browse a restaurant menu organized by food category. The restaurant menu application provides programming examples of the topics discussed in this document.

Please note, the readers of this document are assumed to have basic knowledge of Java programming and Android development concepts. It is not the intention of this document to provide Android tutorial, rather, it focuses on the few essential UI techniques to help developers to achieve the goal of building a dynamic UI.

Also, the code snippets in the document are sample code taken from the restaurant app. The snippets are chosen to illustrate the programming concepts discussed in the document only. They do not provide a complete view of the application structure and details. If you are interested in a comprehensive view of Android application development provided by the sample application, such as life cycle handling of fragments, UI update of grid item selection, retaining the user selection and application data from a fragment during configuration change, or example of UI styling in the resource, please refer to the Android developer links in the References section for details.

Action Bar, Tab, Swipe View, and Dynamic Application Data

While working on the UI for "Restaurant Menu" application, there are a few design considerations in mind:

  1. The UI shall allow users to access essential app features from main screen
  2. The UI shall allow the owner of the restaurant menu application to add/remove food items dynamically
  3. The UI shall keep a consistent presentation and view switching between food categories
  4. The UI shall present the food and information of the menu with images whenever possible
  5. The UI shall allow for the use of gestures that are intuitive to the Android system for screen navigation

The Android action bar, tab, and swipe views are chosen to fulfill the above requirements. In fact, since Android 3.0, these UI elements are one of the most important UI patterns recommended by Android. You will find the use of these UI elements in most of the stock Android applications, such as email, calendar, hangouts, music, and play store. The following screen shots show how we present a browsable restaurant menu with these UI elements.

Figure 1 Screen overview of a restaurant menu application

Creating an action bar and tabs with swipe views

This section describes how you can create an action bar and tabs with swipe views for an Android application.

  1. Add ViewPager to the layout file of the main screen to handle swipe views for tabs (activity_main.xml)
        <android.support.v4.view.ViewPager        
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            android:id="@+id/pager"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".MainActivity" >
    
            <android.support.v4.view.PagerTitleStrip
                android:id="@+id/pager_title_strip"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="top"
                android:background="#333333"
                android:paddingBottom="4dp"
                android:paddingTop="4dp"
                android:textColor="#ffffff"/>        
        </android.support.v4.view.ViewPager>
  2. In the MainActivity.java (the main screen of the application), inflate the activity_main.xml layout, retrieve the ViewPager from the activity layout file, create a ViewPagerAdapter to handle the creation and initialization of each page for the tab, and assign the OnPageChangeListener to the ViewPager
    // Setting up swipe view for each tab
    mViewPager = (ViewPager) findViewById(R.id.pager);
    mViewPager.setOnPageChangeListener(this);
    mPagerAdapter = new PagerAdapter(getSupportFragmentManager(), this);
    mViewPager.setAdapter(mPagerAdapter);
  3. Implement OnPageChangeListener and handle application related tasks during view change. At minimum, the code should set the selected tab on the action bar when users swipe the view to the next tab.
    /**
     * This method is called when the user swipes the tab from one to another
     */
    public void onPageSelected(int position) {
            // on changing the page
            // make respected tab selected
            mActionBar.setSelectedNavigationItem(position);
            mCurrentViewedCategory = (String) mActionBar.getTabAt(position).getText();
    }
    
    /**
     * Tab swipe view related callback
     */
    public void onPageScrolled(int arg0, float arg1, int arg2) {
    }
    
    /**
     * Tab swipe view related callback
     */
    public void onPageScrollStateChanged(int arg0) {
    }
  4. Define PagerAdapter (inherits from FragmentStatePagerAdapter) to handle the view for each tab. Pager Adapter is defined as an inner class to MainActivity.java in the sample. "getItem" is called during the initialization of each page. In this case, each page contains a fragment of the master-detail view of the menu data shown as figure 1. The details of fragment programming are discussed in the next section.

    Tips

    There are two types of PagerAdapterFragmentPagerAdapter and FragmentStatePagerAdapter. For memory efficiency, the former is recommended if the number of the pager is fixed, the latter is recommended if the number of the pager is dynamically allocated. In the case of FragmentStatePagerAdapter, the pager is destroyed when user navigates away from the page. The sample uses FragmentStatePagerAdapter as the number of food category can change based on the application data.

    /**
    * Fragment pager adapter to handle tab swipe view. Each tab view contains 
    * an ultimate fragment which includes a grid menu view and a detail view. 
    * Depending on the orientation of the device, the app decides whether to 
    * show both views or just grid view.
    */
    
    class PagerAdapter extends FragmentStatePagerAdapter {      
        UltimateViewFragment ultimateViewFragment;
        FragmentActivity mFragmentActivity;
        UltimateViewFragment[] fragmentArray = new  
            UltimateViewFragment[mCategory.size()];
            
    public PagerAdapter(FragmentManager fm, FragmentActivity fragmentActivity) {
         super(fm);        
         mFragmentActivity = fragmentActivity;
    }
            
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        super.instantiateItem(container, position);
        UltimateViewFragment fragment = (UltimateViewFragment)     
            super.instantiateItem(container, position);
            fragment.setGridItemListener((GridItemListener) 
            mFragmentActivity);
            fragmentArray[position] = fragment;
            return fragment;
    } 
            
    @Override
    public Fragment getItem(int position) {
        Bundle args = new Bundle();
        // Each ultimate view is associated with one menu category
        args.putString(MenuFactory.ARG_CATEGORY_NAME, 
        mCategory.get(position));
        ultimateViewFragment = new UltimateViewFragment();
        ultimateViewFragment.setArguments(args);
        // Register as a GridItemListener to receive the notification of grid 
        // item click
        ultimateViewFragment.setGridItemListener((GridItemListener) 
        mFragmentActivity);
        fragmentArray[position] = ultimateViewFragment;
        return ultimateViewFragment;
    }
    
    @Override
        public int getCount() {
            // Return number of tabs
            return mCategory.size();
     }
    
    @Override 
        public CharSequence getPageTitle(int position) {
           //Return the title of each tab
            return mCategory.get(position);
        }
    }
  5. Retrieve the ActionBar (mActionBar) from the Activity and set the navigation mode to be NAVIGATION_MODE_TABS
  6. Add tabs to the ActionBar with mActionBar.addTab and initialize the tab title with specified text
    // Setting up action bar and tabs
    mActionBar = getActionBar();
    mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);        
    for (int i = 0; i < mCategory.size(); i++) {
        mActionBar.addTab(mActionBar.newTab().setText(
        mCategory.get(i)).setTabListener(this));
        // Initialize selected items in the hashtable with the first item 
        // of each category
        if (savedInstanceState == null) {
             mSelectedItems.put(mCategory.get(i), mMenuFactory.getMenuWithCategory(
                   mCategory.get(i)).get(0));
        } else {
             //update the mSelectedItems from the last saved instance
             String[] selectedItems = savedInstanceState.getStringArray("selectedItems");
             mSelectedItems.put(mCategory.get(i), mMenuFactory.getMenuItem(
             mCategory.get(i),selectedItems[i]));                
         }
     }

Tips

The ActionBar API was first introduced in Android 3.0 (API level 11), but it is also available in support library for compatibility with Android 2.1 (API level 7) and above. The sample code uses android-support-v4.jar library. The jar is placed in libs folder under application root directory. If you use Eclipse as your IDE, please add the path to the library in Project Properties->Java Build Path->Libraries

Adding action items to action bar

The action bar contains action items that are used frequently by the user. These action items are located on the top of the action bar to allow easy access. In the sample code, we define a few action items, such as camera, search, call, check-out, and settings, as highlighted in the screen shot below.

Figure 2 Action bar with action items

Here is the how you add action items to the action bar.

  1. Define the action items in xml under res/menu (e.g. action_bar.xml)
    <menu xmlns:android="http://schemas.android.com/apk/res/android" >
        
        <item android:id="@+id/action_camera"
              android:icon="@drawable/ic_action_camera"
              android:title="@string/action_camera"
              android:showAsAction="always" />
        <item android:id="@+id/action_search"
              android:title="@string/action_search_str"
              android:icon="@drawable/ic_action_search"
              android:showAsAction="always"
              android:actionViewClass="android.widget.SearchView" />
        <item android:id="@+id/action_call"
              android:icon="@drawable/ic_action_phone"
              android:title="@string/action_call"
              android:showAsAction="always" />
        <item android:id="@+id/action_checkout"
              android:icon="@drawable/ic_action_shoppingcart_checkout"
              android:title="@string/action_checkout"
              android:showAsAction="always"/>
        <item android:id="@+id/action_settings"
            android:orderInCategory="100"
            android:showAsAction="never"
            android:title="@string/action_settings"/>
      
    </menu>
  2. Inflate the action item menu in the code (MainActivity.java)
    /**
    * Initialize the action menu on action bar
    */
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.action_bar, menu);
            
        //Set up the search feature
        SearchManager searchManager =
            (SearchManager) getSystemService(Context.SEARCH_SERVICE);
        SearchView searchView =
            (SearchView) menu.findItem(R.id.action_search).getActionView();
            searchView.setSearchableInfo(
                       searchManager.getSearchableInfo(getComponentName()));
            return super.onCreateOptionsMenu(menu);
    }
  3. Handle the action item click in the code
    /**
    * This method is called when the user click an action from the action bar
    */
    public boolean onOptionsItemSelected(MenuItem item) {
        if (mDrawerToggle.onOptionsItemSelected(item)) {
                  return true;
         }
    
         // Handle presses on the action bar items
         switch (item.getItemId()) {
            // Handle up/home navigation action
            case android.R.id.home:
                NavUtils.navigateUpFromSameTask(this);
                return true;
            // Handle search
            case R.id.action_search:
                        return true;                
            // Handle settings
            case R.id.action_settings:
                        return true;                
            // Handle camera
            case R.id.action_camera:
                        return true;
            //Handle check out feature
            case R.id.action_checkout:
                    return true;
                default:
                    return super.onOptionsItemSelected(item);
    }

Tips

As shown in Figure 1, the action bar contains action items which are frequently performed by users, such as camera, search, and checkout. To keep a consistent look and feel across the system, you can download the Action Bar Icon Pack from https://developer.android.com/design/downloads/index.html and use them in the application resource area.

Action bar styling

Android renders the action bar with a system-defined color which may not always match the color theme of your application. Sometimes, you want to style the action bar with a style and color that is designed for the theme of your application (or business theme color).

For example, the action bar in this sample app is styled with a "maroon" color that matches with the application icon.

Figure 3 Action bar styling example

In this section, we will discuss how to enhance the look of the action bar using Android Action Bar Style Generator.

  1. The tool is available at http://android-ui-utils.googlecode.com/hg/asset-studio/dist/index.html
  2. Specify the color of your choice and download the generated resource files from the link. The following is what I use in the sample app.

    Figure 4 Action bar style generator
  3. The tool generates sample resources icons, images, style xml as the followings. Add all resource files to the application resource drawable area, and update AndroidManifest.xml application:theme to use the style xml generated from the tool.

    Figure 5 Action bar resources example
    <activity
                android:theme="@style/Theme.Example"
                android:name="com.example.restaurant.MainActivity"       
                android:label="@string/app_name" >
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
    </activity>

Handling dynamic application data from the UI

The application UI may not always be static, especially when it has the need to present data that could change during the lifetime of the application. For example, a picture album application allows users to view and edit a number of images that could change any time while the device is on. An email application needs to handle the messages which are refreshed every configurable time interval from the server. In this sample restaurant menu application, the food menu is dynamic. The content of the action bar tab and menu grid could change depending on the food category and menu.

One way to deal with dynamic data is to create a data factory, which acts as a translator between the raw data and the UI. The data factory abstracts out the data manipulation logic from the UI which allows the data to change without changing the UI logic. At a high level, the data factory communicates with data sources for raw application data, processes the raw data from various sources (such as network, local file system, database, or cache), and transforms the raw data into data objects which can be used by the UI components.

The following shows a simple flow among UI, data factory, and data sources.

Figure 6 General flows to handle dynamic application content

Tips

Steps in handling dynamic application content

  • Identify possible data sources, such as network, local file system, database, or device cache
  • Create a data factory that "listens" to the change in data source or query data periodically depending on the needs of the application
  • Data factory handles the request of the data update in a separate thread to avoid blocking of the UI thread as data request and process could take time
  • UI components registers as a data change listener to the Data Factory, and refreshes UI components when a data change notification is received
  • Data Factory manages the listeners of the data change event and provide convenient methods for callers to query data without knowledge of raw data format

To simplify the implementation in the sample restaurant app, food menu data are stored as a string array in Android strings.xml. Each array element contains a record for the food item. In reality, these data records could be defined and stored in a server to allow changes dynamically. Regardless of the source the data, the data format defined in string.xml can be reused. The following shows how application data is processed in MenuFactory.java and how the UI components are initialized with application data.

  1. Define menu data in strings.xml. Each food item contains a string with category name, menu name, description, nutrition facts, price, and the name of the image. The data field is separated by ",,," delimiter.
    <string-array name="menu_array">
    
        <item>Appetizer,,,Angels on horseback,,,Oysters wrapped in bacon, served hot. In the United Kingdom they can also be a savoury, the final course of a traditional British ,,,Calories 393; Fat 22 g( Saturated 4 g); Cholesterol 101 mg; Sodium 836 mg; Carbohydrate 19g; Fiber 3g; Protein 31g,,,6.99,,,Angels on horseback.jpg</item>   
         
        <item>Appetizer,,,Batata vada,,,A popular Indian vegetarian fast food in Maharashtra, India. It literally means potato fritters. The name \"Batata\" means potato in English. It consists of a potato mash patty coated with chick pea flour, then deep-fried and served hot with savory condiments called chutney. The vada is a sphere, around two or three inches in diameter.,,,Calories 393; Fat 22 g( Saturated 4 g); Cholesterol 101 mg; Sodium 836 mg; Carbohydrate 19g; Fiber 3g; Protein 31g,,,7.99,,,Batata vada.jpg</item>
    
        <item>Appetizer,,,Barbajuan,,,An appetizer mainly found in the eastern part of French Riviera and Northern Italy.,,,Calories 393; Fat 22 g( Saturated 4 g); Cholesterol 101 mg; Sodium 836 mg; Carbohydrate 19g; Fiber 3g; Protein 31g,,,8.99,,,Barbajuan.jpg</item>
    
         <item>Appetizer,,,Blooming onion,,,Typically consists of one large onion which is cut to resemble a flower, battered and deep-fried. It is served as an appetizer at some restaurants.,,,Calories 393; Fat 22 g( Saturated 4 g); Cholesterol 101 mg; Sodium 836 mg; Carbohydrate 19g; Fiber 3g; Protein 31g,,,9.99,,,Blooming onion.jpg</item>
    
    </string-array>
  2. During application start-up, MainActivity.java creates a reference to MenuFactory as a singleton and loads the data from the Android resource area.
    mMenuFactory = MenuFactory.getInstance(res);
    mMenuFactory.loadDataFromAndroidResource();
  3. MenuFactory processes the data from strings.xml and transforms them to MenuItem objects used by UI views.
    /* Allows caller to load the app data from Android resource area */
    public void loadDataFromAndroidResource() {    
        if (mMenuItems != null && mMenuItems.size() > 0) {
            clear();
        }        
        mMenuItems = new ArrayList<MenuItem>();
        mCategoryList = new ArrayList<String>();
            
        String[] menuList = mResources.getStringArray(R.array.menu_array);
        MenuItem menuItem;
        String[] currentMenu;
        String currentCategory = "";
            
        for (int i = 0; i<menuList.length; i++) {
        currentMenu = menuList[i].split(",,,");
        menuItem = new MenuItem();
        for (int j = 0; j< currentMenu.length; j++) {
            switch (j) {
            case 0:
                menuItem.setCategory(currentMenu[j]);
                if (!currentMenu[j].equals(currentCategory)) {
                    currentCategory = currentMenu[j];
                    mCategoryList.add(currentMenu[j]);
                }
                break;
            case 1:
                menuItem.setName(currentMenu[j]);                                        break;
            case 2:
                menuItem.setDescription(currentMenu[j]);
                break;
            case 3:
                menuItem.setNutrition(currentMenu[j]);                                        break;
            case 4:
                menuItem.setPrice(currentMenu[j]);                                        break;    
            case 5:
                menuItem.setImageName(currentMenu[j]);                                        break;
            }
                   }
                   menuItem.setId(Integer.toString(i));        
                   mMenuItems.add(menuItem);
        }    
    }
  4. MenuFactory.java provides convenient methods for callers to query data related to the UI updates.
    /* Allows caller to retrieve the category list based on menu items */
    public ArrayList<String> getCategoryList() {
        return mCategoryList;
    }
        
    /* Allows caller to retrieve a list of menu based on passed in category */
    public ArrayList<MenuItem> getMenuWithCategory (String category) {
        ArrayList<MenuItem> result = new ArrayList<MenuItem>();
        for (int i = 0; i<mMenuItems.size(); i++) {
            MenuItem item = mMenuItems.get(i);
            if (item.category.equals(category)) {
                result.add(item);
            }            
        }
            
        return result;
    }
        
    /* Allows caller to retrieve menu item based on passed category and index */
    public MenuItem getMenuItem (String category, int index) {
        ArrayList<MenuItem> menuList = getMenuWithCategory(category);
        if (menuList.size() == 0) {
            return null;
        }
        return menuList.get(index);
    }
        
    /* Allows caller to retrieve menu item based on passed category and name */
    public MenuItem getMenuItem (String category, String name) {
        MenuItem result = null;
        for (int i = 0; i<mMenuItems.size(); i++) {
            MenuItem item = mMenuItems.get(i);
            if (item.category.equals(category) && item.name.equals(name)) {
                result = item;
            }            
        }    
        return result;
    }
    
    /* Data structure for menu item */
    class MenuItem {
        String category;
        String name;
        String price;
        String description;
        String nutrition;
        ImageView image;
        String imageName;
        String id;
            
        public void setCategory(String str) {
            category = str;
        }
            
        public void setName(String str) {
            name = str;
        }
        
        public void setDescription(String str) {
            description = str;
        }
            
        public void setNutrition(String str) {
            nutrition = str;
        }
            
        public void setImageName(String str) {
            imageName = str;
        }
            
        public void setId(String str) {
            id = str;
        }
            
        public void setPrice(String str) {
            price = str;
        }
            
        public void setImageView(ImageView imageView) {
            image = imageView;
        }
    }
  5. Initialize the grid menu view with food items from MenuFactory. The implementation is done in MenuGridFragment.java. In the restaurant application, food items for each category are displayed in a GridView which is embedded in a Fragment. During the fragment initialization, MenuFactory is called to retrieve the food items for each category. The data adapter (ImageAdapter) of the grid view creates and renders each food item during the initialization.
    /* Inflating view item for the grid view and initialize the image adapter
    * for the grid view. 
    */
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
            
        View rootView = inflater.inflate(
            R.layout.fragment_menu_grid, container, false);
    
        mMenuList = mMenuFactory.getMenuWithCategory(mCategory);    
        mGridView = (GridView) rootView.findViewById(R.id.gridview);
        mGridView.setAdapter(mImageAdapter);
        mGridView.setOnItemClickListener(this);
        return rootView;
    }
    
        /* Image adapter for the grid view */
        class ImageAdapter extends BaseAdapter {
            private LayoutInflater mInflater;
    
            public ImageAdapter(Context c) {
                mInflater = LayoutInflater.from(c);
            }
    
            public int getCount() {
                return mMenuList.size();
            }
    
            public Object getItem(int position) {
                return mMenuList.get(position);
            }
    
            public long getItemId(int position) {
                return position;
            }
                
            // create a new ImageView for each item referenced by the Adapter
            public View getView(int position, View convertView, ViewGroup parent) {
    
                View v = convertView;
                ImageView picture;
                TextView name;
                TextView price;
    
                if(v == null) {
                    v = mInflater.inflate(R.layout.view_grid_item, parent, false);
                    v.setTag(R.id.picture, v.findViewById(R.id.picture));
                    v.setTag(R.id.grid_name, v.findViewById(R.id.grid_name));
                    v.setTag(R.id.grid_price, v.findViewById(R.id.grid_price));
                }
             
                picture = (ImageView)v.getTag(R.id.picture);
                name = (TextView)v.getTag(R.id.grid_name);
                price = (TextView) v.getTag(R.id.grid_price);
    
                MenuItem item = (MenuItem) mMenuList.get(position);
                
                InputStream inputStream = null;
                AssetManager assetManager = null;
                try {
                    assetManager = getActivity().getAssets();
                    inputStream =  assetManager.open(item.imageName);
                    picture.setImageBitmap(BitmapFactory.decodeStream(inputStream));
                } catch (Exception e) {
                } finally {      
                }
            
                name.setText(item.name);
                price.setText(item.price);
                
                //Highlight the item if it's been selected        
                if (mSelectedPosition == position){
                        updateGridItemColor(v, true);         
                } else {
                        updateGridItemColor(v, false);    
                }
    
                return v;
            }    
        }
  6. Initialize action bar tabs with food categories from MenuFactory
    // Retrieve the category list from MenuFactory 
    mCategory = mMenuFactory.getCategoryList();
    // Setting up action bar and tabs
    mActionBar = getActionBar();
    mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);        
    for (int i = 0; i < mCategory.size(); i++) {
    mActionBar.addTab(mActionBar.newTab().setText(
        mCategory.get(i)).setTabListener(this));
         // Initialize selected items in the hashtable with the first item 
         // of each category
         if (savedInstanceState == null) {
              mSelectedItems.put(mCategory.get(i), mMenuFactory.getMenuWithCategory(
                            mCategory.get(i)).get(0));
            } else {
                 //update the mSelectedItems from the last saved instance
                 String[] selectedItems = savedInstanceState.getStringArray("selectedItems");
                 mSelectedItems.put(mCategory.get(i), mMenuFactory.getMenuItem(
                          mCategory.get(i),selectedItems[i]));                
            }
    }

Android Fragment, Multi-Pane Layout, and Master-Detail View

Another aspect of a Dynamic UI is how you can design the UI of your Android application so that the same application will fit and flow well on devices with difference sizes (such as tablets and phones). In this section, we will discuss the use of an Android fragment to design multi-pane layout for devices with different display sizes.

Android introduces the concept of "fragment" in 3.0. You can view "fragment" as a way to "componentize" your screen layout. The screen is componentized into multiple UI groups or views. Each UI group or view is implemented as a fragment. Your application decides which UI groups/views and navigation flow is available to users during run time based on the display of the device it is running on.

Multi-pane layouts are a common usage of an Android fragment, in which the screen is presented as a combination of multiple views. Interaction with one view on a screen could result in the update of another view on the screen. Master-detail view is one of the important UI design patterns for this concept. The application presents the overview of the content in a master view with a list or grid view widget. Selecting an item on the grid or list shows the detail view of the item on the same screen or a different screen. On a device with a large display (tablet), both master and detail view could fit on the same screen. On a device with a smaller screen (phone), master and detail view could be presented on different screens.

The restaurant menu app presents menu information for each food category in a grid view. Selecting the grid item shows the detail of the food item on the same screen if the device has a large display, or a different screen if the device has a smaller screen. This design is implemented in 3 Fragments:

  1. UltimateVIewFragment – A fragment which contains a grid view fragment and a detail view fragment, the visibility of inner fragments are determined during run time based on the size of screen (e.g. detail view is only shown if the device has big display)
  2. GridViewFragment – A fragment that presents the menu data for each food category in a grid view
  3. DetailViewFragment – A fragment that presents the detail of a food item selected in the grid view

Tips

Most of the code samples on the Android developer site show the implementation of master-detail view with two Fragments embedded inside an Activity, but not with two fragments embedded inside a fragment. In the sample restaurant app, we implemented the latter one. The use of a fragment, instead of an activity, is required by the swipe view of action bar tabs. The sample restaurant code shows you in detail how this can be done with tab swipe view.

Figure 7 Multi-pane, master-detail view for device with different screen sizes

Creations of fragments

This sections describes the steps required to create fragments used in the sample.

  1. Define the screen layout with a fragment in XML. The following are screen layouts for UltimateVIewFragment, GridViewFragment, and DetailViewFragment. Please note, the visibility of the detail view below is set to "gone" initially, it should be changed in the respective layout file for devices of different screen sizes. For example, if the device has a larger display, the visibility is set to "visible". See section "Multi-pane and single-pane layouts based on screen size" later on for details.

    This section shows screen layout for UltimateViewFragment only. Please see the complete code sample for screen layout for GridViewFragment (layout/fragment_menu_grid.xml) and DetailViewFragment (layout/fragment_disch_detail.xml).

    \
    <!&mdash;UltimateViewFragment screen layout -->
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:orientation="horizontal">
        
        <FrameLayout 
            android:id="@+id/grid_fragment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"/>
            
        <FrameLayout 
            android:id="@+id/detail_fragment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:visibility="gone"
            android:orientation="vertical"/>
        
     </LinearLayout>
  2. Create and initialize fragments programmatically and use FragmentManager to handle the transactions of fragments. The following code snippet shows the implementation of UltimateViewFragment which creates MenuGridFragment and DetailViewFragment during run time. For details to implement MenuGridFragment and DetailViewFragment, please see the complete sample code.
    public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
                   
            if (savedInstanceState != null) {  
                String tmp = savedInstanceState.getString("selectedIndex");
                if (tmp != null) {
                    mSelectedIndex = Integer.parseInt(tmp);
                }
                mCategory = savedInstanceState.getString("currentCategory", mCategory);
            } else {
                mCategory = (String) getArguments().getString(MenuFactory.ARG_CATEGORY_NAME);
            }
            mDetailViewFragment = new DetailViewFragment();
            mGridViewFragment = new MenuGridFragment();
                    
            mGridViewFragment.setCategory(mCategory);
            mGridViewFragment.setOnGridItemClickedListener(this);   
                
            FragmentManager fragmentManager = this.getChildFragmentManager();
            FragmentTransaction transaction = fragmentManager.beginTransaction();
            
            if (savedInstanceState != null) {      
                transaction.replace(R.id.detail_fragment, mDetailViewFragment);
                transaction.replace(R.id.grid_fragment, mGridViewFragment);
            } else {
                transaction.add(R.id.detail_fragment, mDetailViewFragment, "detail_view");
                transaction.add(R.id.grid_fragment, mGridViewFragment, "grid_view");
            }
            transaction.commit();  
    }

Tips

The fragments should be created during run time of an activity, especially if you plan to swap fragments in and out of a screen dynamically. Fragments can be added, replaced, and removed from the screen using FragmentManager. As shown in the code, the FragmentManager can be retrieved from getChildFragmentManager, not getFragmentManager, since the container of the children fragments is a fragment, not an Activity. Also, during an orientation change, to avoid adding the same fragments on top of each other in UltimateViewFragment, the code should replace the existing fragments with the new fragment using "replace", not "add" from FragmentTransaction.

Communication between fragments and activities

The communication between fragments can be handled by using the listener pattern, which involves two easy steps:

  1. Defining a listener interface that can be implemented by components which are interested in receiving the notification from other components.
  2. In the case of multi-pane and master-detail fragments, if a child fragment is sending the notification, the container of the child fragments will register as a listener to the child fragment. Upon receiving of the notification, the parent fragment or activity can take appropriate actions based on the received information.

Here is how it is done in the restaurant app:

  1. Define a GridItemListener interface. The interface is implemented by the container of the grid fragment. The grid fragment notifies the parent container when a grid selection happens.
    /**
     * An interface implemented by classes which want to receive notification
     * when a menu item is clicked on the grid. This interface is used by 
     * UltimateViewFragment, ActionBarActivity, DetailView to communicate the selected
     * menu item.
    */
    public interface GridItemListener {
        public void onGridItemClick(com.example.restaurant.MenuFactory.MenuItem itemSelected, int position);
    }
  2. UltimateViewFragment provides a method for caller to register itself as GridItemListener.
    /* Allow caller to set the grid item listener */
    public void setGridItemListener(GridItemListener gridItemListener) {
        mGridItemListener = gridItemListener;
    }
  3. UltimateViewFragment notifies its listener of a change in grid selection.
    /* Handle the event of item click from the menu grid */
    public void onGridItemClick(MenuItem itemSelected, int position) {
        mGridItemListener.onGridItemClick(itemSelected, position);
        mSelectedIndex = position;
        View detail = getActivity().findViewById(R.id.detail_fragment);
        //portrait mode
        if (detail != null && detail.getVisibility() == View.GONE) {
            Intent intent = new Intent(this.getActivity(), DetailActivity.class);
                        intent.setAction("View");
                        intent.putExtra("category", itemSelected.category);
                      intent.putExtra("entree_name", itemSelected.name);
                        Activity activity = getActivity();
                        activity.startActivity(intent);        
                
                //landscape mode
        } else {
            mDetailViewFragment.update(itemSelected);
        }
    }
  4. In the MainActivity, each tab view is the container parent of the UltimateViewFragment. MainActivity registers itself as the GridItemListener to keep track of the last selected food item for each category.
    @Override
    public Object instantiateItem(ViewGroup container, int position) {
    super.instantiateItem(container, position);
    UltimateViewFragment fragment = (UltimateViewFragment) 
    super.instantiateItem(container, position);
        fragment.setGridItemListener((GridItemListener) 
        mFragmentActivity);
    fragmentArray[position] = fragment;
    return fragment;
    }
  5. MainActivity takes appropriate actions in the listener callback when a notification is received.
    /**
     * This method is called when a grid menu item is clicked
    */
    public void onGridItemClick(com.example.restaurant.MenuFactory.MenuItem 
        itemSelected, int position) {
    mSelectedItems.put(itemSelected.category, itemSelected);
    }

Multi-pane and single-pane layouts based on screen size

As discussed above, an Android fragment allows you to define multi-pane layouts for screens. But, how do we use screen size to determine multiple-pane/single-pane layouts, and how do we provide the different screen flows based on the multiple-pane/single-pane design? The Android resource system provides configuration qualifiers for the application to handle multiple screen layouts.

Different layout files are provided in the resource system based on screen sizes. During application start-up, Android will apply the appropriate layout files based on the display size. Before Android 3.2, you can define the layout for small, normal, large, or xlarge screens sizes. Layout files can be defined in res/layout-small for devices with a screen size smaller than 426dpx320dp, or res/layout-xlarge for screen sizes which are larger than 960dpx720dp. The following is the definition of these screen sizes.

  • xlarge screens are at least 960dp x 720dp
  • large screens are at least 640dp x 480dp
  • normal screens are at least 470dp x 320dp
  • small screens are at least 426dp x 320dp

Here is how each size is mapped to the actual device size in inches.

Figure 8 Mapping of screen size qualifier to actual display size in inches

Starting in Android 3.2, the above screen size qualifier has been replaced with the use of qualifier sw<N>dp where N is the pixel definition for the screen width. For example, for "large" screen, the layout files can be provided in layout-sw600dp directory.

We can further add "portrait" or "landscape" definition in the resource qualifier. For example, I may want to have a special layout for screen width 600dp and portrait mode only. In this case, a layout-sw600dp-port will be created to store the layout.

The following is the layout structure based on screen size and orientation of the device used in the sample application. For a mid-size tablet, I want to have single-pane layout for portrait mode since the UI may be squeezed if we use multi-pane layout on a mid-size tablet (7" or 8" tablet).

Figure 9 Multiple layout files in resource system for different display sizes

The fragment_ultimate_view.xml layout for different screen sizes is largely the same. This only thing that is different is the visibility of the child fragment. For a mid-size tablet, the ultimate view layout will look like this:

<!&mdash;UltimateViewFragment screen layout &agrave;
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal">
    
    <FrameLayout 
        android:id="@+id/grid_fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"/>
        
    <FrameLayout 
        android:id="@+id/detail_fragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:visibility="gone"
        android:orientation="vertical"/>
    
 </LinearLayout>

How do we handle the navigation flow for different layouts? During the application run time, based on the visibility of the view, the application can decide whether to update other views on the same screen (multi-pane) or to start a new screen (single-pane). The following code snippet shows how this is done:

/* Handle the event of item click from the menu grid */
public void onGridItemClick(MenuItem itemSelected, int position) {
    mGridItemListener.onGridItemClick(itemSelected, position);
    mSelectedIndex = position;
    View detail = getActivity().findViewById(R.id.detail_fragment);
    //portrait mode
    if (detail != null && detail.getVisibility() == View.GONE) {
        Intent intent = new Intent(this.getActivity(), DetailActivity.class);
                    intent.setAction("View");
                    intent.putExtra("category", itemSelected.category);
                  intent.putExtra("entree_name", itemSelected.name);
                    Activity activity = getActivity();
                    activity.startActivity(intent);        
            
            //landscape mode
    } else {
        mDetailViewFragment.update(itemSelected);
    }
}

Fragment life cycle handling

Just like Android Activity, fragments implement the lifecycle callbacks and take proper actions during the start, pause, resume, stop, and destroy states of an application. The lifecycle management for a fragment is similar to that of an Activity.

Tips

Besides the regular Activity life cycle callbacks, such as onCreate, onStart, onResume, onPause, onStop, and onDestroy, fragments have a few extra lifecycle callbacks:

onAttach() – Called when the fragment has been associated with the activity

onCreateView() – Called to create the view hierarchy associated with the fragment

onActivityCreated() – Called when the activity’s onCreate() has returned

onDestroyView() – Called when the view hierarchy associated with the fragment is being removed

onDetach() – Called when the fragment is being disassociated from the activity

The following shows the effect of an activity life cycle on fragment life cycle callbacks.

Figure 10 Mapping of application lifecycle to fragment lifecycle callbacks

Similar to Activity, fragments may need to save and restore application states during device configuration changes, such as a change in device orientation, or an unexpected Destroyed activity to a Paused activity. Saving application states will be handled in the onSaveInstanceState () callback, which is called before the Activity is destroyed, and restoring application state can be handled in either the onCreate (), onCreateView (), or onActivityCreated () callback. The following code snippet shows how the restaurant app saves the index of the selected grid item in onSaveInstanceState () and restores it in onCreate ().

public void onCreate (Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
        
    if (savedInstanceState != null) {
                    String selectedIndex = savedInstanceState.getString("selectedIndex");
                   if(selectedIndex != null) {            
                        mSelectedPosition = Integer.parseInt(selectedIndex);
                    }
            }      
        
    mImageAdapter = new ImageAdapter(getActivity());
}
    
/* Saved the last selected Index before orientation change */
public void  onSaveInstanceState (Bundle outState) {
            //only save the selected position if user has clicked on the item
            if (mSelectedPosition != -1) {
                    outState.putString("selectedIndex", Integer.valueOf(mSelectedPosition).toString());
            }
}

Tips

There may be times that you don’t want your fragments to be recreated during configuration change (device orientation change), perhaps due to the complexity of the application state data. Calling setRetainInstance (true) on the container class of fragments can prevent fragment from being recreated during configuration change.

Android Resource System, graphics and text, screen resolution and density

What about text and graphics presentation on screen? A font size chosen for a phone device may be too small for a tablet with a large screen. A graphic icon may look just right on a tablet, but appear too big on a phone with a smaller screen. And, what about preventing an image from being stretched on devices with different screen resolutions?

In this section, we will discuss a few techniques to make sure your text and graphics resources look well on screen with different resolutions and densities.

Images and screen pixel density

Screen pixel density plays an important role in how graphics is presented on the screen. A same image will show up bigger on a screen with low pixel density, simply because the size of one pixel is larger on a low density device. Vice versa, the same image will show up smaller on a screen with high pixel density. When you design your UI, you want to preserve the same look of the graphics as much as possible across devices with different screen pixel densities.

In most cases, this can be handled by using a pixel-independent unit, such as "dp" and "wrap_content" in specifying the dimension and layout of the graphics icon in the resource system. With a pixel-independent unit, Android will adjust the graphics based on the pixel density of the screen. The following sample demonstrates the use of "dp" and "wrap_content" in showing the image of food items in a detailed view.

<ImageView
    android:id="@+id/dish_image"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:layout_below="@+id/cart_area"
    android:minWidth="600dp"
    android:minHeight="400dp"
android:scaleType="centerCrop"/>

Sometimes this is not sufficient. For example, a small image could look pixelated on a screen due to the image scaling. In this case, you should provide alternative images in the res/drawable area to avoid the problem. Images with different sizes can be provided in res/drawable-<xxxxxx> folders, in which xxxxxx is the generalized density category. The following chart provides a reference to interpret the generalized density in actual screen density.

Figure 11 Different drawable areas with graphics resources for devices with different display sizes

Figure 12 Mapping of generalized screen density to actual dpi value

Size of text and screen sizes

To enhance the readability of the text, sometimes it is necessary to adjust the font size based on the size of the display. For example, in the sample restaurant app, I use a smaller font size for the name and price of the food on the grid menu for a device with less than 600 pixels in screen width (like a phone). I created a text "style" with different font sizes for screens with larger and smaller displays. The style files are stored in res/values-sw<N>dp with respect to the screen size.

Figure 13 Different style files for screen with different sizes

The following style file specifies the font size of the text used in the grid menu item.

<!&mdash;text style for screen with dp smaller than 600 &agrave;   
 <style name="GridItemText">        
        <item name="android:textColor">@color/grid_item_unselected_text</item>
        <item name="android:textStyle">italic</item>
        <item name="android:textSize">14sp</item>
    </style>

<!&mdash;text style for screen with dp larger than 600 &agrave;   
<style name="GridItemText">        
        <item name="android:textColor">@color/grid_item_unselected_text</item>
        <item name="android:textStyle">italic</item>
        <item name="android:textSize">20sp</item>
    </style>

The layout file for the grid item reference to the text style as defined above (view_grid_item.xml).

<TextView
            android:id="@+id/grid_price"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingLeft="5dp"
            android:paddingRight="5dp"
            android:paddingTop="15dp"
            android:paddingBottom="15dp"
            android:layout_gravity="right"
 style="@style/GridItemText"/>

Conclusion

Android UI is one of the most interesting areas in Android programming. There are many things to consider when designing and programming Android UI. This document discusses four essential concepts that help you to achieve the goal of building a Dynamic UI:

  1. Use of the latest recommended UI elements, such as the action bar, tab, and swipe view for screen navigation.
  2. Programming practices to handle dynamic application data and how they are used with the action bar, tab, and swipe views.
  3. Using Android fragments to implement multi-pane and master-detail view layouts for devices with different screen sizes
  4. Using the Android resource system to improve the look of graphics and text on a screen with different resolutions and pixel densities

As Android continues evolving. A good practice is to embrace the latest UI techniques and to keep your knowledge up to date with the latest UI concepts. Having these techniques in mind will help you with your dynamic UI design for the many Android devices to come.

References

  1. Action bar creation: https://developer.android.com/training/basics/actionbar/index.html
  2. Action bar styling: https://developer.android.com/training/basics/actionbar/styling.html
  3. Multi-pane layouts: https://developer.android.com/design/patterns/multi-pane-layouts.html
  4. Fragment programming guide: https://developer.android.com/guide/components/fragments.html
  5. Multiple screen design: https://developer.android.com/guide/practices/screens_support.html
  6. Android SDK reference: https://developer.android.com/reference/packages.html

Related Articles and References

The products described in this document may contain design defects or errors known as errata which may cause the product to deviate from published specifications. Current characterized errata are available on request.

Contact your local Intel sales office or your distributor to obtain the latest specifications and before placing your product order.

Copies of documents which have an order number and are referenced in this document, or other Intel literature, may be obtained by calling 1-800-548-4725, or go to:

http://www.intel.com/design/literature.htm

Software and workloads used in performance tests may have been optimized for performance only on Intel microprocessors. Performance tests, such as SYSmark* and

MobileMark*, are measured using specific computer systems, components, software, operations, and functions. Any change to any of those factors may cause the results to vary.

You should consult other information and performance tests to assist you in fully evaluating your contemplated purchases, including the performance of that product when combined with other products.

**This sample source code is released under the Intel Sample Source Code License Agreement

To learn more about Intel tools for the Android developer, visit Intel® Developer Zone for Android.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here