为 Android* 设备构建动态 UI





0/5 (0投票)
本文档将重点介绍一些安卓UI编程技术,这些技术将帮助您实现动态UI的目标——使用操作栏、选项卡和滑动视图与动态应用程序数据相结合,以增强屏幕导航;使用安卓片段设计多窗格和主-
摘要
“动态UI”对安卓*开发者意味着什么?作为安卓应用开发者,您希望您的应用程序UI能够适应应用程序内容的动态特性。您还希望您的应用程序UI能够在大多数安卓设备上良好地适应和流动,无论显示屏尺寸、屏幕分辨率或像素密度如何。鉴于市场上安卓设备的种类繁多以及跟上最新安卓SDK的挑战,这可能是一项艰巨的任务。
本文档将重点介绍一些安卓UI编程技术,这些技术将帮助您实现动态UI的目标——使用操作栏、选项卡和滑动视图与动态应用程序数据相结合,以增强屏幕导航;使用安卓片段设计多窗格和主视图布局,以适应不同显示尺寸的屏幕;以及使用安卓资源系统改进图形和文本内容在不同分辨率和密度屏幕上的呈现效果。
目录
引言
本文档从3个方面讨论动态UI的构建
- 使用最新的安卓UI模式和控件显示动态内容——您至少希望您的应用程序UI能够像其他安卓系统一样“活”起来、“呼吸”起来。一个关键是使用最新的安卓UI组件、控件和导航模式来设计您的应用。一定程度的定制不是问题,但总的来说,您希望遵循最新的安卓指南和趋势。那么,我们如何使用这种最新的UI趋势来呈现动态应用程序内容呢?在本文中,我们将演示如何使用最新的安卓UI组件,如操作栏、选项卡和滑动视图来呈现动态应用程序数据。
- UI布局和导航流程——4英寸手机和11英寸平板电脑使用相同的UI布局是否合理?为了最大化大显示屏的空间,有时您可能需要考虑为不同显示尺寸的设备使用不同的布局。在本文档中,我们将讨论如何使用安卓片段设计多窗格/单窗格布局和主-详情视图,以适应不同显示尺寸的屏幕。
- 显示分辨率和密度——除了UI布局,如果您的应用使用图形,您如何确保图形在不同屏幕分辨率和像素密度的设备上不会拉伸或像素化?文本项的字体大小如何?字体大小为20的文本项在手机上可能看起来很完美,但在平板电脑上可能太小。我们将讨论如何使用安卓资源系统来处理这些问题。
示例餐厅菜单应用
为了说明本文档中描述的编程概念,我编写了一个安卓应用程序,允许用户浏览按食物类别组织的餐厅菜单。该餐厅菜单应用程序提供了本文档中讨论主题的编程示例。
请注意,本文档的读者应具备Java编程和安卓开发概念的基础知识。本文档的目的不是提供安卓教程,而是专注于少数必要的UI技术,以帮助开发人员实现构建动态UI的目标。
此外,文档中的代码片段是取自餐厅应用程序的示例代码。这些片段仅用于说明文档中讨论的编程概念。它们不提供应用程序结构和细节的完整视图。如果您对示例应用程序提供的安卓应用程序开发的全面视图感兴趣,例如片段的生命周期处理、网格项选择的UI更新、在配置更改期间保留片段的用户选择和应用程序数据,或者资源中UI样式的示例,请参阅参考资料部分的安卓开发者链接以获取详细信息。
操作栏、选项卡、滑动视图和动态应用程序数据
在开发“餐厅菜单”应用程序的UI时,有一些设计考虑因素
- UI应允许用户从主屏幕访问基本应用功能
- UI应允许餐厅菜单应用程序的所有者动态添加/删除食物项目
- UI应在食物类别之间保持一致的呈现和视图切换
- UI应尽可能用图像呈现菜单中的食物和信息
- UI应允许使用安卓系统直观的手势进行屏幕导航
选择安卓操作栏、选项卡和滑动视图来满足上述要求。事实上,自安卓3.0以来,这些UI元素是安卓推荐的最重要的UI模式之一。您会在大多数安卓原生应用程序中发现这些UI元素的使用,例如电子邮件、日历、环聊、音乐和Play商店。以下屏幕截图展示了我们如何使用这些UI元素呈现可浏览的餐厅菜单。
创建带滑动视图的操作栏和选项卡
本节描述如何为安卓应用程序创建带滑动视图的操作栏和选项卡。
- 在主屏幕的布局文件(activity_main.xml)中添加
ViewPager
来处理选项卡的滑动视图<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>
- 在MainActivity.java(应用程序的主屏幕)中,加载activity_main.xml布局,从activity布局文件中检索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);
- 实现
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) { }
- 定义
PagerAdapter
(继承自FragmentStatePagerAdapter
)来处理每个选项卡的视图。在示例中,PagerAdapter被定义为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); } }
- 从Activity中检索
ActionBar
(mActionBar
)并将导航模式设置为NAVIGATION_MODE_TABS
- 使用
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级别11)中引入,但也存在于支持库中,以兼容Android 2.1(API级别7)及更高版本。示例代码使用了android-support-v4.jar库。jar文件放在应用程序根目录下的libs文件夹中。如果您使用Eclipse作为IDE,请在项目属性->Java构建路径->库中添加库的路径
向操作栏添加操作项
操作栏包含用户经常使用的操作项。这些操作项位于操作栏顶部,方便访问。在示例代码中,我们定义了一些操作项,例如相机、搜索、呼叫、结账和设置,如下面屏幕截图所示。
以下是向操作栏添加操作项的方法。
- 在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>
- 在代码中加载操作项菜单(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); }
- 在代码中处理操作项点击事件
/** * 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.cn/design/downloads/index.html下载Action Bar Icon Pack并在应用程序资源区域中使用它们。
操作栏样式
安卓使用系统定义的颜色渲染操作栏,这可能并不总是与您应用程序的颜色主题匹配。有时,您希望使用为应用程序主题(或业务主题颜色)设计的样式和颜色来样式化操作栏。
例如,此示例应用中的操作栏被设置为与应用程序图标匹配的“栗色”。
本节将讨论如何使用安卓操作栏样式生成器来增强操作栏的外观。
- 该工具可在此处获取:http://android-ui-utils.googlecode.com/hg/asset-studio/dist/index.html
- 指定您选择的颜色并从链接下载生成的资源文件。以下是我在示例应用程序中使用的内容。图4 操作栏样式生成器
- 该工具生成示例资源图标、图像、样式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、数据工厂和数据源之间的简单流程。
技巧
处理动态应用程序内容的步骤
- 识别可能的数据源,例如网络、本地文件系统、数据库或设备缓存
- 创建一个数据工厂,根据应用程序的需求“监听”数据源的变化或定期查询数据
- 数据工厂在一个单独的线程中处理数据更新请求,以避免阻塞UI线程,因为数据请求和处理可能需要时间
- UI组件注册为数据工厂的数据更改监听器,并在收到数据更改通知时刷新UI组件
- 数据工厂管理数据更改事件的监听器,并为调用者提供便捷的方法来查询数据,而无需了解原始数据格式
为了简化示例餐厅应用中的实现,食物菜单数据以字符串数组形式存储在Android strings.xml中。每个数组元素包含一个食物项目的记录。实际上,这些数据记录可以定义并存储在服务器中,以允许动态更改。无论数据来源如何,string.xml中定义的数据格式都可以重复使用。以下显示了MenuFactory.java中如何处理应用程序数据以及UI组件如何使用应用程序数据进行初始化。
- 在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>
- 在应用程序启动期间,MainActivity.java创建
MenuFactory
的单例引用,并从安卓资源区域加载数据。mMenuFactory = MenuFactory.getInstance(res); mMenuFactory.loadDataFromAndroidResource();
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); } }
- 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; } }
- 使用
MenuFactory
中的食物项目初始化网格菜单视图。此实现位于MenuGridFragment.java中。在餐厅应用程序中,每个类别的食物项目都显示在一个嵌入在片段中的GridView中。在片段初始化期间,会调用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; } }
- 使用
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])); } }
安卓片段、多窗格布局和主-详情视图
动态UI的另一个方面是您如何设计安卓应用程序的UI,使其能够在不同尺寸的设备(如平板电脑和手机)上良好地适应和流动。在本节中,我们将讨论如何使用安卓片段为具有不同显示尺寸的设备设计多窗格布局。
安卓在3.0中引入了“片段”的概念。您可以将“片段”视为“组件化”屏幕布局的一种方式。屏幕被组件化为多个UI组或视图。每个UI组或视图都实现为一个片段。您的应用程序根据其运行设备的显示屏在运行时决定哪些UI组/视图和导航流程可供用户使用。
多窗格布局是安卓片段的一种常见用法,其中屏幕以多个视图的组合形式呈现。屏幕上一个视图的交互可能会导致屏幕上另一个视图的更新。主-详情视图是此概念的重要UI设计模式之一。应用程序在主视图中通过列表或网格视图小部件呈现内容的概览。选择网格或列表上的项目会在同一屏幕或不同屏幕上显示项目的详情视图。在具有大显示屏的设备(平板电脑)上,主视图和详情视图可以同时显示在同一屏幕上。在具有较小屏幕的设备(手机)上,主视图和详情视图可以在不同的屏幕上呈现。
餐厅菜单应用以网格视图显示每个食物类别的菜单信息。如果设备具有大显示屏,则选择网格项会在同一屏幕上显示食物项目的详细信息;如果设备具有较小屏幕,则会在不同的屏幕上显示。此设计通过3个片段实现
UltimateVIewFragment
– 一个包含网格视图片段和详情视图片段的片段,内部片段的可见性在运行时根据屏幕尺寸确定(例如,详情视图仅在设备具有大显示屏时显示)GridViewFragment
– 一个在网格视图中呈现每个食物类别的菜单数据的片段DetailViewFragment
– 一个呈现网格视图中选定食物项目详情的片段
技巧
安卓开发者网站上的大多数代码示例都显示了主-详情视图的实现,其中包含嵌入在Activity中的两个片段,而不是嵌入在片段中的两个片段。在示例餐厅应用程序中,我们实现了后者。操作栏选项卡的滑动视图需要使用片段而不是Activity。示例餐厅代码详细展示了如何通过选项卡滑动视图实现这一点。
片段的创建
本节描述了创建示例中使用的片段所需的步骤。
- 在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>
- 以编程方式创建和初始化片段,并使用
FragmentManager
处理片段的事务。以下代码片段显示了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(); }
技巧
片段应该在活动的运行时创建,特别是如果您计划动态地在屏幕中交换片段。片段可以使用FragmentManager
添加、替换和移除。如代码所示,FragmentManager
可以从getChildFragmentManager
而不是getFragmentManager
中检索,因为子片段的容器是片段,而不是活动。此外,在方向改变期间,为了避免在UltimateViewFragment
中将相同的片段相互叠加,代码应该使用FragmentTransaction
中的“replace”而不是“add”来替换现有片段与新片段。
片段与活动之间的通信
片段之间的通信可以通过使用监听器模式来处理,这涉及两个简单的步骤
- 定义一个监听器接口,可以由对接收来自其他组件的通知感兴趣的组件实现。
- 在多窗格和主-详情片段的情况下,如果子片段正在发送通知,则子片段的容器将注册为子片段的监听器。收到通知后,父片段或活动可以根据收到的信息采取适当的行动。
以下是餐厅应用中的实现方式
- 定义一个
GridItemListener
接口。该接口由网格片段的容器实现。当发生网格选择时,网格片段会通知父容器。/** * 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); }
UltimateViewFragment
提供了一个方法供调用者将自身注册为GridItemListener
。/* Allow caller to set the grid item listener */ public void setGridItemListener(GridItemListener gridItemListener) { mGridItemListener = gridItemListener; }
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); } }
- 在
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; }
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); }
基于屏幕尺寸的多窗格和单窗格布局
如上所述,安卓片段允许您为屏幕定义多窗格布局。但是,我们如何使用屏幕尺寸来确定多窗格/单窗格布局,以及如何根据多窗格/单窗格设计提供不同的屏幕流程?安卓资源系统为应用程序提供了配置限定符来处理多个屏幕布局。
资源系统根据屏幕尺寸提供不同的布局文件。在应用程序启动期间,安卓将根据显示尺寸应用适当的布局文件。在安卓3.2之前,您可以为小型、普通、大型或超大型屏幕尺寸定义布局。布局文件可以定义在res/layout-small中,适用于屏幕尺寸小于426dpx320dp的设备,或res/layout-xlarge中,适用于屏幕尺寸大于960dpx720dp的设备。以下是这些屏幕尺寸的定义。
- 超大屏幕至少为 960dp x 720dp
- 大屏幕至少为 640dp x 480dp
- 普通屏幕至少为 470dp x 320dp
- 小屏幕至少为 426dp x 320dp
以下显示了每种尺寸如何映射到实际设备尺寸(英寸)。
从安卓3.2开始,上述屏幕尺寸限定符已被使用限定符sw<N>dp取代,其中N是屏幕宽度的像素定义。例如,对于“大”屏幕,布局文件可以提供在layout-sw600dp目录中。
我们可以在资源限定符中进一步添加“portrait”或“landscape”定义。例如,我可能希望为屏幕宽度为600dp且仅为竖屏模式的屏幕提供特殊布局。在这种情况下,将创建一个layout-sw600dp-port来存储布局。
以下是示例应用程序中根据屏幕尺寸和设备方向的布局结构。对于中型平板电脑,我希望竖屏模式下使用单窗格布局,因为如果在中型平板电脑(7英寸或8英寸平板电脑)上使用多窗格布局,UI可能会被挤压。
针对不同屏幕尺寸的fragment_ultimate_view.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>
我们如何处理不同布局的导航流程?在应用程序运行时,根据视图的可见性,应用程序可以决定是更新同一屏幕上的其他视图(多窗格)还是启动新屏幕(单窗格)。以下代码片段显示了如何实现这一点
/* 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);
}
}
片段生命周期处理
就像安卓Activity一样,片段实现了生命周期回调,并在应用程序的启动、暂停、恢复、停止和销毁状态期间采取适当的操作。片段的生命周期管理类似于Activity的生命周期管理。
技巧
除了常规的 Activity 生命周期回调,如 onCreate
、onStart
、onResume
、onPause
、onStop
和 onDestroy
,片段还有一些额外的生命周期回调
onAttach()
– 片段与活动关联时调用
onCreateView()
– 创建与片段关联的视图层次结构时调用
onActivityCreated()
– 活动的onCreate()返回后调用
onDestroyView()
– 移除与片段关联的视图层次结构时调用
onDetach()
– 片段与活动解除关联时调用
以下显示了活动生命周期对片段生命周期回调的影响。
与Activity
类似,片段可能需要在设备配置更改期间保存和恢复应用程序状态,例如设备方向更改,或从暂停活动到意外Destroyed
活动。保存应用程序状态将在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());
}
}
技巧
有时您可能不希望您的片段在配置更改(设备方向更改)期间被重新创建,这可能是因为应用程序状态数据的复杂性。在片段的容器类上调用setRetainInstance (true)可以防止片段在配置更改期间被重新创建。
安卓资源系统、图形和文本、屏幕分辨率和密度
文本和图形在屏幕上的呈现效果如何?为手机设备选择的字体大小可能对于大屏幕平板电脑来说太小。图形图标在平板电脑上可能看起来恰到好处,但在小屏幕手机上可能显得太大。那么,如何防止图像在不同屏幕分辨率的设备上拉伸呢?
在本节中,我们将讨论几种技术,以确保您的文本和图形资源在不同分辨率和密度的屏幕上看起来良好。
图像和屏幕像素密度
屏幕像素密度在图形如何在屏幕上呈现方面起着重要作用。同一张图片在低像素密度屏幕上会显得更大,仅仅因为在一个低密度设备上一个像素的大小更大。反之,同一张图片在高像素密度屏幕上会显得更小。在设计UI时,您希望在不同屏幕像素密度的设备上尽可能保持图形的相同外观。
在大多数情况下,这可以通过在资源系统中指定图形图标的尺寸和布局时使用独立于像素的单位(如“dp”和“wrap_content”)来处理。通过独立于像素的单位,安卓将根据屏幕的像素密度调整图形。以下示例演示了在详细视图中显示食物项目图像时使用“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是通用密度类别。下图提供了将通用密度解释为实际屏幕密度的参考。
文本大小和屏幕尺寸
为了增强文本的可读性,有时需要根据显示尺寸调整字体大小。例如,在示例餐厅应用中,我为屏幕宽度小于600像素(如手机)的设备上的网格菜单中食物的名称和价格使用了较小的字体大小。我创建了一个具有不同字体大小的文本“样式”,以适用于较大和较小显示屏的屏幕。样式文件存储在res/values-sw<N>dp中,具体取决于屏幕尺寸。
以下样式文件指定了网格菜单项中使用的文本的字体大小。
<!—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>
网格项的布局文件引用了上面定义的文本样式(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"/>
总结
安卓UI是安卓编程中最有趣的领域之一。在设计和编程安卓UI时需要考虑许多因素。本文档讨论了四个基本概念,它们可以帮助您实现构建动态UI的目标
- 使用最新推荐的UI元素,例如操作栏、选项卡和滑动视图进行屏幕导航。
- 处理动态应用程序数据的编程实践以及它们如何与操作栏、选项卡和滑动视图一起使用。
- 使用安卓片段为不同屏幕尺寸的设备实现多窗格和主-详情视图布局
- 使用安卓资源系统改进图形和文本在不同分辨率和像素密度屏幕上的显示效果
随着安卓不断发展。一个好的做法是拥抱最新的UI技术,并及时更新您对最新UI概念的知识。牢记这些技术将有助于您为即将推出的众多安卓设备设计动态UI。
参考资料
- 操作栏创建:https://developer.android.com.cn/training/basics/actionbar/index.html
- 操作栏样式:https://developer.android.com.cn/training/basics/actionbar/styling.html
- 多窗格布局:https://developer.android.com.cn/design/patterns/multi-pane-layouts.html
- 片段编程指南:https://developer.android.com.cn/guide/components/fragments.html
- 多屏幕设计:https://developer.android.com.cn/guide/practices/screens_support.html
- 安卓SDK参考:https://developer.android.com.cn/reference/packages.html
相关文章和参考资料
本文档中描述的产品可能包含已知为勘误的设计缺陷或错误,这可能导致产品偏离已发布的规范。当前的已表征勘误可应要求提供。
请联系您当地的英特尔销售办事处或您的经销商以获取最新的规范,并在下订单前进行咨询。
可以通过拨打1-800-548-4725或访问以下网址获取本文档中提及的具有订单号的文档或其他英特尔文献:
http://www.intel.com/design/literature.htm
性能测试中使用的软件和工作负载可能仅针对英特尔微处理器进行了性能优化。性能测试,例如 SYSmark* 和
MobileMark*,是使用特定的计算机系统、组件、软件、操作和功能测量的。这些因素中的任何更改都可能导致结果有所不同。
您应该查阅其他信息和性能测试,以帮助您充分评估您考虑的购买,包括该产品与其他产品结合时的性能。
**此示例源代码根据英特尔示例源代码许可协议发布
要了解更多关于安卓开发者的英特尔工具,请访问英特尔® 安卓开发者专区。