为 Android* 设备构建动态 UI

下载文档

下载为 Android* 设备构建动态 UI [PDF 1MB]

摘要

“动态 UI”对于 Android* 开发人员意味着什么? 作为 Android 应用开发人员,你可能希望你的应用 UI 能够适应应用内容的动态环境。 你可以进行一定程度的定制,但一般来说最好能够符合最新的 Android 准则和趋势。 但是,这可能是一件困难的事情,因为市场上存在多种多样的 Android 设备,而且你还需要与最新的 Android SDK 保持同步。

在本文中,我们将主要介绍几种 Android UI 编程技术帮助您达成动态 UI 目标,包括使用包含动态应用数据的操作栏、标签和滑动视图来增强屏幕导航,使用 Android fragment 为使用不同尺寸显示器的屏幕设计多窗口和主视图布局,以及使用 Android 资源系统提高使用不同分辨率和密度的屏幕的图形和文本内容呈现。

目录

  1. 简介
  2. 示例餐厅菜单应用
  3. 操作栏、标签、滑动视图和动态应用数据
  4. Android UI fragment、多窗口和主从视图
  5. Android 资源系统、图形和文本、屏幕分辨率和密度
  6. 结论
  7. 参考文献

简介

本文从三个方面讨论了动态 UI 的构建:

  1. 使用最新的 Android UI 模式和控制显示动态内容 — 至少,你希望你的应用 UI 像其他 Android 系统一样“生存”和“呼吸”。 关键一点是使用最新的 Android UI 组件、控制和导航模式来设计应用。 你可能希望,能够进行一定程度的定制,而且总体上紧跟最新的 Android 准则和趋势。 那么,我们如何使用此最新 UI 趋势呈现动态应用内容呢? 在本文中,我们将演示如何使用最新 Android UI 组件,如操作栏、标签和滑动视图,来呈现动态应用数据。
  2. UI 布局和导航流程 – 为 4 英寸的手机和 11 英寸的平板电脑使用相同的 UI 布局是否可行? 为了最大限度地利用大显示器的空间,有时,你希望为显示器尺寸不同的设备使用不同的布局。 在本文中,我们将讨论如何使用 Android Fragment 设计多窗口/单窗口布局和主从视图,以适应拥有不同显示器尺寸的屏幕。
  3. 显示器分辨率和密度 — 除了 UI 布局之外,如果你在应用中使用了图形,如何确保在采用不同屏幕分辨率和像素密度的设备上图形不被拉伸或像素化? 文本项的字号如何处理? 文本项采用 20 号字体在手机上可能非常合适,但是在平板电脑上可能会太小。 我们将讨论如何使用 Android 资源系统来处理这些问题。

示例餐厅菜单应用

为了解释本文中介绍的编程概念,我编写了一款可帮助用户浏览按照食物分类安排的餐厅菜单的应用。 餐厅菜单应用可为本文中讨论的话题提供编程示例。

请注意,本文面向对 Java 编程和 Android 开发概念的基本知识有一定了解的读者。 本文不提供 Android 教程,而主要介绍几种基本的 UI 技术帮助开发人员达成动态 UI 的构建目标。

此外,本文中的代码片段是取自餐厅应用的示例代码。 我们选取这些代码,仅为解释本文中摘录的编程内容。 它们并不提供应用结构和细节的完整视图。 如果您希望全面了解该示例应用提供的 Android 应用开发,如 fragment 的生命循环处理、单元格项选择的 UI 更新、在配置更改时保留 fragment 中的用户选择和应用数据或资源中的 UI 风格示例,请参见“参考文献”部分的 Android 开发人员链接,了解更多信息。

操作栏、标签、滑动视图和动态应用数据

在为“餐厅菜单”应用设计 UI 时,我考虑到以下几个方面:

  1. UI 应支持用户从主窗口访问基本应用功能
  2. UI 应支持餐厅菜单应用的所有者动态添加/删除食物项目
  3. UI 在切换食物分类时应可提供一致的外观和视图
  4. UI 应可在需要时呈现带有图片的食物和菜单信息
  5. UI 应支持使用手势进行界面导航,这种方式在 Android 系统中非常直观

我选择了 Android 操作栏、标签和滑动视图来满足上述要求。 事实上,自 Android 3.0 起,这些 UI 元素成为 Android 推荐的重点 UI 模式。 你会发现大部分的常用 Android 应用中都使用了这些 UI 元素,如电子邮件、日历、hangouts、音乐和 play store。 下面的截屏展示了我们如何使用这些 UI 元素呈现可浏览的餐厅菜单。



图 1 餐厅菜单应用的屏幕概观

使用滑动视图创建操作栏和标签

本部分内容介绍了如何为 Android 应用创建带有操作栏和标签的滑动视图。

 

  1. 向主屏幕的布局文件中添加 ViewPager,以便处理标签的滑动视图(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. 在 MainActivity.java(应用的主界面)中,activity_main.xml 布局进行 inflate 处理,从活动布局文件中检索 ViewPager,创建 ViewPagerAdapter 以处理每页标签的创建和初始化,并将 OnPageChangeListener 分配至 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. 执行 OnPageChangeListener,在视图切换时处理与应用相关的任务。 至少,当用户将视图滑动至下一个标签时,代码应可在操作栏上设置选择的标签。
    /**
     * 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. 对 PagerAdapter(继承自 FragmentStatePagerAdapter)进行定义,以处理每个标签的视图。 示例中,Pager Adapter 被定义为 MainActivity.java 的内部类。 初始化每个页面时将会调用 “getItem”。 在本案例中,每一页包括一个含有菜单数据的主从视图的碎片,如图 1 所示。 碎片编程的具体内容将会在下一部分进行介绍。

    技巧

    编程中共有两种 PagerAdapter:FragmentPagerAdapter 和 FragmentStatePagerAdapter。 从内存效率角度来看,如果页面数固定则推荐使用前者;如果动态分配页面数则推荐使用后者。 如果使用了 FragmentStatePagerAdapter,当用户离开页面时,页面调度程序就会被破坏。 本示例使用了 FragmentStatePagerAdapter,因为食物种类的数量能够随着应用数据变化。

    /**
    * 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. 从 Activity 中检索 ActionBar (mActionBar),并将导航模式设置为 NAVIGATION_MODE_TABS
  6. 使用 mActionBar.addTab 向 ActionBar 添加标签,并使用指定文本初始化标签标题
    // 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]));	    	    
         }
     }
    
    

技巧

ActionBar API 在 Android 3.0 (API level 11) 中首次推出,但是由于具备对 Android 2.1 (API level 7) 及更高版本的兼容性,它也能够在支持的库中使用。 本示例代码使用了 android-support-v4.jar 库。 此 jar 格式文件位于应用根目录下的库文件夹中。 如果您使用 Eclipse 作为 IDE,请将路径添加至 Project Properties->Java Build Path->Libraries 中的库

向操作栏中添加操作项

操作栏中包含用户经常使用的操作项。 这些操作项位于操作栏顶部,以便于用户轻松访问。 在样本代码中,我们对几个操作项进行了定义,如摄像头、调用、查看和设置,请见下面截屏中的突出显示。



图 2 带有操作项的操作栏

以下介绍了如何向操作栏中添加操作项。

  1. 在 res/menu 下的 xml 中定义操作项(如 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 操作项菜单(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. 在代码中处理操作项触发(click)
    /**
    * 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);
    }
    
    

技巧

如图 1 所示,操作栏中包含用户经常执行的操作项,如摄像头、搜索和查看。 如要确保整个系统中的外观和风格一致,你可以从 https://developer.android.com/design/downloads/index.html 下载操作栏图示包(Action Bar Icon Pack)并将其用于应用资源区。

操作栏风格

Android 使用系统定义的颜色来渲染操作栏,这种颜色有时可能与你的应用颜色主题不相配。 有时,你可能想要使用专为应用主题设计的风格和颜色(或企业主题颜色)来设计操作栏。

例如,本示例应用中的操作栏使用了与应用主题相匹配的“栗色”。


图 3 操作栏风格示例

在本部分中,我们将介绍如何使用 Android Action Bar Style Generator 来增强操作栏的外观。

  1. 如欲获取该工具,请访问: http://android-ui-utils.googlecode.com/hg/asset-studio/dist/index.html
  2. 指定选择的颜色并通过链接下载生成的资源文件。 以下是我在本示例应用中所使用的颜色和风格。



    图 4 操作栏风格生成工具

  3. 该工具生成的示例资源图示、图像、风格 xml 文件如下所示。 向应用资源 drawable 区域中添加所有资源文件,然后更新 AndroidManifest.xml application:theme,以使用该工具生成的风格 xml 文件。



    图 5 操作栏资源示例

    <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>
    
    

处理 UI 中的动态应用数据

应用 UI 并非一直都是静态状态,尤其当它呈现的数据可能在应用生命周期内发生变化时。 例如,图片集应用支持用户查看和编辑大量图片,而这些图片在设备开启时随时可能会发生更改。 电子邮件应用需要处理的消息在每个服务器所配置的时间段内都会刷新一次。 在该样本餐厅菜单应用中,食物菜单是动态的。 操作栏标签和菜单格的内容可以随着食物种类和菜单而变化。

处理动态数据的一种方法是创建数据工厂;数据工厂相当于原始数据和 UI 之间的转换器。 数据工厂可以从 UI 中抽取数据操作逻辑,从而能够在不改变 UI 逻辑的情况下改变数据。 从更高层面而言,数据工厂能够与数据来源通信以获取原始应用数据,处理多种来源(如网络、本地文件系统、数据库或缓存)的原始数据,并将原始数据转化为 UI 组件可以使用的数据对象。

下图简要介绍了 UI、数据工厂和数据来源之间的流程。



图 6 处理动态应用内容的一般流程

技巧

处理动态应用内容的步骤

  • 确认可能的数据来源,如网络、本地文件系统、数据库或设备缓存
  • 创建一个数据工厂,“倾听”数据来源的变化,或根据应用的需求定期查询数据
  • 数据工厂在单独的线程中处理数据更新请求以避免阻止 UI 线程,因为数据请求和处理需要耗费一定的时间
  • UI 组件登记为数据工厂的数据变更监听器,收到数据变更通知后,刷新 UI 组件
  • 数据工厂可管理数据变更事件的监听器,并提供便捷的方式以便调用程序(caller)在不了解原始数据格式情况下也可查询数据

为了简化样本餐厅应用中的实施,我们将食物菜单数据在 Android strings.xml 中存储为一个字符串阵列。 每个阵列元素包含一个食物项记录。 在实际情况下,这些数据记录可在服务器中进行定义和存储,以便能够进行动态变更。 无论数据的来源为何处,数据格式都可以在 string.xml 中重复使用。 下文介绍了应用如何在 MenuFactory.java 中进行处理,以及如何使用应用数据初始化 UI 组件。

  1. 在 strings.xml 中定义菜单数据。 每个食物项目中都包括一个含有种类名称、菜单名称、介绍、营养成分、价格和图片名称的字符串。 数据字段由分隔符 “,,,” 进行划分。
    <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. 在应用启动过程中, MainActivity.java 将创建一个单独的 MenuFactory 参考,并加载 Android 资源区域的数据。
    mMenuFactory = MenuFactory.getInstance(res);
    mMenuFactory.loadDataFromAndroidResource();
    
  3. MenuFactory 处理来自 strings.xml 的数据,并将其转换为 UI 视图使用的 MenuItem 对象。
    /* 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 可提供便捷的方式,以便调用程序查询 UI 更新相关信息。
    /* 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. 使用 MenuFactory 的食物项初始化网格菜单视图。 此操作是在 MenuGridFragment.java 中完成。 在菜单应用中,每种类型中的食物项都通过内嵌于 Fragment 中的 GridView 显示 。 在 fragment 初始化过程中,将会调用 MenuFactory 检索每类中的食物项。 网格视图的数据适配器(ImageAdapter)可在初始化过程中创建和渲染每个食物项。
    /* 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. 使用 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、多窗口布局和主从视图

动态 UI 的另一个重点是如何设计 Android 应用的 UI 才能让相同的应用适用于不同尺寸的设备(如平板电脑和手机)。 在本部分内容中,我们将讨论如何使用 Android fragment 为显示器尺寸不同的设备设计多窗口布局。

Android 在 3.0 中引入 “fragment” 的概念。 你可以把 “fragment” 看作 “组件化”屏幕布局的一种方式。 屏幕被划分为多个 UI 组或视图。 每个 UI 组或视图将作为一个 fragment 来实施。 你的应用将根据其所在设备的显示器来决定在运行时为用户使用哪种 UI 组/视图和导航流程。

多窗口布局是 Android fragment 的一种常见用途,在这种布局中,屏幕呈现为组合的多视图。 与屏幕中的一个视图交互可能会更新屏幕上的另一个视图。 主从视图便是此概念的一个重要 UI 设计模式。 该应用使用一个列表或网格视图 widget 在主视图中呈现总体内容。 选择网格或列表中的一个项目,将会在同一个屏幕或另一个屏幕上显示出该项目的细节视图。 在采用大尺寸显示器的设备(平板电脑)上,主视图和细节视图能够在相同的屏幕上显示。 在采用较小尺寸显示器的设备(手机)上,主视图和细节视图能够在不同的屏幕中呈现。

餐厅菜单应用在网格视图中呈现每类食物的菜单信息。 如果设备为大尺寸显示器,选择网格项将会在同一屏幕上显示食物项的详细内容;如果设备为较小的屏幕,则会显示在另一个屏幕中显示。 该设计通过 3 个 Fragment 实现:

  1. UltimateVIewFragment — 包含一个网格视图 fragment 和一个细节视图 fragment 的 fragment;内部 fragment 的可见性将取决于运行时屏幕的尺寸(如果设备采用大尺寸显示器,则仅显示细节视图)
  2. GridViewFragment — 在网格视图中呈现每类食物菜单数据的 fragment
  3. DetailViewFragment — 在网格视图中呈现选中的食物项的细节的 fragment

技巧

Android 开发人员网站上的大部分代码样本仅展示了将两个 Fragment 嵌入一个 Activity 中的主从视图的操作,而没有在一个 fragment 内嵌入两个 fragment 的操作。 在样本餐厅应用中,我们执行了第二种方式。 操作栏的滑动视图要求使用 fragment 而非 activity。 样本餐厅代码将详细展示如何使用标签滑动视图实现该操作。



图 7 适用于采用不同屏幕尺寸的设备的多窗口和主从视图

fragment 的创建

本部分内容将介绍创建本样本中使用的 fragment 所需的步骤。

  1. 使用 fragment 在 xml 中定义屏幕布局。 以下代码是 UltimateVIewFragment、GridViewFragment 和 DetailViewFragment 的屏幕布局。 请注意,以下细节视图的可见性最初设置为 “gone”,在不同屏幕尺寸的设备的布局文件中应分别对其进行更改。 例如,如果设备拥有更大的显示器,可见性应设置为 “visible”。 请参见下面的“基于屏幕尺寸的多窗口和单窗口布局”部分,了解更多信息。

    本部分内容仅展示了 UltimateViewFragment 的屏幕布局。 请参见 GridViewFragment (layout/fragment_menu_grid.xml) 和 DetailViewFragment (layout/fragment_disch_detail.xml) 的屏幕布局的完整代码样本。

    <!—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. 以编程方式创建和初始化 fragment,并使用 FragmentManager 处理 fragment 的交易。 以下代码片段展示了 UltimateViewFragment 的执行; UltimateViewFragment 可在运行过程中创建 MenuGridFragment 和 DetailViewFragment。 关于执行 MenuGridFragment 和 DetailViewFragment 的详细信息,请参见完整的样本代码。
    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();  
    }
    

技巧

fragment 应在一项活动运行时创建,尤其如果准备以动态方式将 fragment 从屏幕中调入或调出时。 你可以使用 FragmentManager 在屏幕中添加、更换和删除 Fragment。 如代码中所示,FragmentManager 能够从 getChildFragmentManager 而非 getFragmentManager 中检索到,因为子 fragment 的容器是 fragment 而非 Activity。 此外,在改变方向时,为了避免在 UltimateViewFragment 中在彼此上面添加相同的 fragment,代码应用使用 FragmentTransaction 中的 “replace” 而非 “add” ,用新的 fragment 替换现有的 fragment。

fragment 和活动之间的通信

fragment 之间的通信可以通过使用监听器模式来实现,这包括两个简单的步骤:

  1. 定义一个监听器界面,这可以通过希望从其他组件接收通知的组件来执行。
  2. 在使用多窗口和主从 fragment 的情况下,如果子 fragment 发送通知,子 fragment 的容器将注册为子 fragment 的监听器。 收到通知后,母 fragment 或活动能够根据接收到的信息执行相应的操作。

以下是餐厅应用中的做法:

  1. 定义一个 GridItemListener 界面。 该界面通过网格 fragment 容器来实现。 当网格选择操作发生时,网格 fragment 将通知母容器。
    /**
     * 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 支持调用程序将其注册为 GridItemListener。
    /* Allow caller to set the grid item listener */
    public void setGridItemListener(GridItemListener gridItemListener) {
    	mGridItemListener = gridItemListener;
    }
    
    
  3. UltimateViewFragment 通知其监听器网格选择中的变化。
    /* 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. 在 MainActivity 中,每个标签视图都是 UltimateViewFragment 的母视图。 MainActivity 将其自身注册为 GridItemListener,以追踪上一个在每类中选中的食物项。
    @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 将在监听器回调中采用相应的操作。
    /**
     * 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);
    }
    
    

基于屏幕尺寸的多窗口和单窗口布局

如上文所述,Android fragment 可帮助你为屏幕定义多窗口布局。 但是,如何使用屏幕尺寸确定多窗口/单窗口布局,如何根据多窗口/单窗口设计提供不同的屏幕流程呢? Android 资源系统可为应用提供处理多界面布局的配置限定符。

根据屏幕尺寸,资源系统中将提供不同的布局文件。 在应用启动过程中,Android 将根据显示器尺寸运用相应地布局文件。 如果采用 Android 3.2 之前的版本,你可以为小型、正常、大型或超大型屏幕定义布局。 对于屏幕尺寸小于 426dpx320dp 的设备,布局文件可以在 res/layout-small 中进行定义;对于屏幕尺寸大于 960dpx720dp 的设备,可以在 res/layout-xlarge 中定义。 以下是这些屏幕尺寸的定义。

  • 超大尺寸最低为 960dp x 720dp
  • 大型尺寸最低为 640dp x 480dp
  • 正常尺寸最低为 470dp x 320dp
  • 小型尺寸最低为 426dp x 320dp

下图展示了每种尺寸与以英寸计的实际设备尺寸的关系。



图 8 屏幕尺寸限定符与以英寸计的实际显示器尺寸的关系

自 Android 3.2 起,以上屏幕尺寸限定符被限定符 sw<N>dp 取代;其中的 N 是指屏幕看度的像素定义。 例如,对于“大型”屏幕,可从 layout-sw600dp 目录中获得布局文件。

我们可以进一步在资源限定符中添加 “portrait” 或 “landscape” 定义。 例如,我只想为屏幕宽度为 600dp 的纵向模式进行特别布局。 在这种情况下,需要创建 layout-sw600dp-port 来存储布局。

以下是基于样本应用中使用的屏幕尺寸和设备方向的布局结构。 对于中等尺寸的平板电脑,我希望为纵向模式设置单窗口布局,因为如果在中等尺寸的平板电脑(7 英寸或 8 英寸的平板电脑)上使用多窗口布局,UI 可能会被挤压。



图 9 不同显示器尺寸在资源系统中的多布局文件

不同尺寸的 fragment_ultimate_view.xml 布局大体相同。 唯一的不同是子 fragment 的可见性。 对于中等尺寸的平板电脑,最终的视图布局如下:

<!—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>

如何处理不同布局的导航流程? 在应用运行时,根据视图的可见性,应用可以决定是否需要相同的屏幕(多窗口)上更新其他视图,还是重新打开一个屏幕(但窗口)。 以下代码片段展示了这是如何实现的:
/* 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 生命周期处理

如同 Android Activity,fragment 在应用的启动、暂停、恢复、停止和破坏状态中执行生命周期回调和相应操作。 fragment 的生命周期管理与 Activity 相似。

技巧

除了常规的 Activity 生命周期回调,如 onCreate、onStart、onResume、onPause、onStop 和 onDestroy 之外,fragment 很少有其他的生命周期回调:

onAttach() – 当 fragment 与活动相关时调用

onCreateView() – 创建与 fragment 相关的视图层级时调用

onActivityCreated() – 当返回活动的 onCreate() 时调用

onDestroyView() – 当与 fragment 相关的视图层级被删除时调用

onDetach() – 当 fragment 与活动断开关联时调用

下图展示了活动生命周期对 fragment 生命周期回调的影响。


图 10 应用生命周期与 fragment 生命周期回调之间的关系

与 Activity 相同,fragment 需要在设备配置变更过程中对应用状态进行保存和恢复,如设备方向的变化、意外破坏活动或暂停活动。 保存应用状态需要在 onSaveInstanceState () 回调中进行处理,它应在 Activity 破坏之前调用;恢复应用状态可在 onCreate ()、onCreateView () 或 onActivityCreated () 回调中进行处理。 以下代码片段展示了餐厅应用如何在 onSaveInstanceState () 中保存选中的网格项索引并在 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());
        	}
}

技巧

有时,由于应用状态数据的复杂性,你可能不希望在配置变化(设备方向变化)时重新创建 fragment。 在 fragment 的容器类中调用 setRetainInstance (true) 能够阻止 fragment 在配置变化时重新创建。

Android 资源系统、图形和文本、屏幕分辨率和密度

屏幕上的文本和图形呈现如何处理? 针对手机设备选择的字体对于采用大屏幕的平板电脑可能太小。 图形图标可能适合平板电脑,但是在屏幕较小的手机上则会太大。 如何防止图像在屏幕分辨率不同的设备上被拉伸?

在本部分内容中,我们将介绍几种能够确保文本和图形资源在采用不同分辨率和密度的屏幕上正常显示的技术。

图像和屏幕像素密度

屏幕像素密度对于图形在屏幕上的呈现非常重要。 因为每像素的尺寸在低像素密度设备上更大,所以同一个图像会在低像素密度的屏幕上显示得更大。 相反,同一个图像在采用高像素密度的屏幕上将显示得更小。 在设计 UI 时,你可能会希望在采用不同屏幕像素密度的设备上尽可能保留相同的图像外观。

大部分情况下,这可以通过使用像素独立的单元来实现,如使用 “dp” 和 “wrap_content” 在资源系统中指定图像图示的尺寸和布局。 借助像素独立的单元,Android 能够根据屏幕的像素密度来调整图像。 以下样本展示了如何使用 “dp” 和 “wrap_content” 在细节视图中展示食物项的图像。

<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"/>

有时候,仅通过这种方式无法完成。 例如,由于图像伸展,小图像可能在屏幕上出现像素化。 在这种情况下,你应该在 res/drawable 区域提供可替代的图像以避免这种问题。 采用不同尺寸的图像可在 res/drawable-<xxxxxx> 文件夹中提供;其中 xxxxxx 是通用的密度类型。 下图介绍了实际屏幕密度中的通用密度,以供参考。



图 11 各种 drawable 区域,包含采用不同尺寸显示器的设备的图形资源



图 12 通用屏幕密度在实际 dpi 值中的映射

文本大小和屏幕尺寸

为了增强文本的可辨认性,有时需要根据显示器的尺寸来调整字体。 例如,在样本餐厅应用中,对于屏幕宽度小于 600 像素的设备(如手机),我在网格菜单中为食物的名称和价格采用了较小的字号。 我创建了一种文本 “style” ,能够在采用较大和较小显示器的屏幕中使用不同的字号。 该 style 文件存储在与屏幕尺寸有关的 res/values-sw<N>dp 中。



图 13 不同尺寸屏幕的不同 style 文件

以下 style 文件指定了网格菜单项中使用的文本的字体大小。

<!—text style for screen with dp smaller than 600    
 <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>
<!—text style for screen with dp larger than 600    
<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> 

网格项的布局文件引用了上面定义的文本 style(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"/>

结论

Android UI 是 Android 编程中最有趣的领域。 设计和编程 Android UI 时需要考虑许多因素。 本文讨论了四种基本概念,以帮助你实现构建动态 UI 的目标:

  1. 在屏幕导航中使用最新推荐的 UI 元素,如操作栏、标签和滑动视图。
  2. 如何处理动态应用数据以及将它们运用到操作栏、标签和滑动视图中的编程实践。
  3. 使用 Android fragment 为采用不同尺寸屏幕的设备执行多窗口和主从视图布局
  4. 使用 Android 资源系统增强采用不同分辨率和像素密度的屏幕的图形和文本的外观

随着 Android 不断发展, 明智的做法是积极采用最新的 UI 技术,并及时了解最新的 UI 概念。 掌握这些技术将会帮助你为未来出现的各种 Android 设备轻松设计动态 UI。

参考文献

  1. 操作栏创建: https://developer.android.com/training/basics/actionbar/index.html
  2. 操作栏风格: https://developer.android.com/training/basics/actionbar/styling.html
  3. 多窗口布局: https://developer.android.com/design/patterns/multi-pane-layouts.html
  4. Fragment 编程指南: https://developer.android.com/guide/components/fragments.html
  5. 多屏幕设计: https://developer.android.com/guide/practices/screens_support.html
  6. Android SDK 参考: https://developer.android.com/reference/packages.html

关于作者

Mei-Lin Hsieh 是一位在英特尔及其他公司的移动开发领域拥有 15 年工作经验的软件工程师。 她目前就职于软件解决方案事业部,主要负责为平板电脑和手机上的 Android 应用提供大规模项目支持。

相关文章与文献:


本文件所描述的产品可能包含使其与宣称的规格不符的设计缺陷或失误。 这些缺陷或失误已收录于勘误表中,可索取获得。

在发出订单之前,请联系当地的英特尔营业部或分销商以获取最新的产品规格。

如欲获得本文涉及的带编号文档的副本或其他英特尔文献,可致电 1-800-548-4725,或访问:

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

在性能检测过程中涉及的软件及其性能只有在英特尔微处理器的架构下方能得到优化。 性能测试(如 SYSmark* 和

MobileMark*)使用特定的计算机系统、组件、软件、操作和功能进行测量。 上述任何要素的变动都有可能导致测试结果的变化。

请参考其他信息及性能测试(包括结合其他产品使用时的运行性能)以对目标产品进行全面评估。

** 该示例源代码根据英特尔示例源代码许可协议发布

有关编译器优化的更完整信息,请参阅优化通知
类别: