`
因为我们有更多的屏幕空间可以使用,所以有几个额外的按钮供用户交互。
现在,当我们运行应用时,你可以看到 Android 如何选择合适的布局来匹配我们的配置,无论是在手机上纵向和横向(参见图 2–13),还是在更大的平板电脑屏幕上运行(参见图 2–14)。
图 2–13。 纵向和横向布局
图 2–14。 大型(平板)布局
后期添加
在 API Level 9 (Android 2.3)中,增加了一个资源限定符来支持“超大”屏幕:
根据 SDK 文档,传统的“大”屏幕大约在 5 到 7 英寸的范围内。“超大”的新定义涵盖了大约 7 到 10 英寸以上的屏幕。
如果您的应用是针对 API Level 9 构建的,那么您也应该将您的平板电脑布局包含在 res/layout-xlarge/目录中。请记住,运行 Android 2.2 或更早版本的表只会将 res/layout-large/识别为有效的限定符。
2–20。自定义键盘操作
问题
您想要自定义软键盘的 enter 键的外观、用户点击它时发生的操作或两者。
解
(API 三级)
为使用键盘输入数据的 widget 自定输入法(IME)选项。
它是如何工作的
自定义回车键
当键盘在屏幕上可见时,return 键上的文本通常具有基于视图中可聚焦项目顺序的动作。当未指定时,如果视图中有更多可移动的焦点,键盘将显示“下一个”动作,或者如果当前关注最后一个项目,键盘将显示“完成”动作。然而,通过在视图的 XML 中设置android:imeOptions
值,可以为每个输入视图定制这个值。下面列出了您可以设置来自定义 return 键的值:
actionUnspecified:默认显示设备选择的动作
actionGo:显示“Go”作为回车键
actionSearch:显示一个搜索窗口作为 return 键
actionSend:显示“Send”作为回车键
actionNext:显示“Next”作为返回键
actionDone:显示“Done”作为返回键
让我们看一个带有两个可编辑文本字段的示例布局,如清单 2–47 所示。第一个将在 return 键上显示搜索玻璃,第二个将显示“Go”
**清单 2–47。**EditText 小工具上带有自定义输入选项的布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <EditText android:id="@+id/text1" android:layout_width="fill_parent" android:layout_height="wrap_content" android:imeOptions="actionSearch" /> <EditText android:id="@+id/text2" android:layout_width="fill_parent" android:layout_height="wrap_content" android:imeOptions="actionGo" /> </LinearLayout>
键盘的最终显示会有所不同,因为一些制造商特定的 UI 套件包括不同的键盘,但在纯 Google UI 上的结果将显示为如图图 2–15。
图 2–15。 回车键 自定义输入选项的结果
**注意:**自定义编辑器选项仅适用于软输入法。更改该值不会影响用户在物理硬件键盘上按 return 键时生成的事件。
自定义动作
定制当用户按下回车键时发生的事情和调整它的显示一样重要。覆盖任何动作的默认行为只需要将一个TextView.OnEditorActionListener
附加到感兴趣的视图。让我们继续上面的示例布局,这次为两个视图添加一个自定义动作(参见清单 2–48)。
清单 2–48。 活动实现自定义键盘动作
`public class MyActivity extends Activity implements OnEditorActionListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//Add the listener to the views
EditText text1 = (EditText)findViewById(R.id.text1);
text1.setOnEditorActionListener(this);
EditText text2 = (EditText)findViewById(R.id.text2);
text2.setOnEditorActionListener(this);
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if(actionId == IME_ACTION_SEARCH) {
//Handle search key click
return true;
}
if(actionId == IME_ACTION_GO) {
//Handle go key click
return true;
}
return false;
}
}`
布尔返回值onEditorAction()
告诉系统您的实现是否已经使用了该事件,或者是否应该将它传递给下一个可能的响应者(如果有的话)。当您的实现处理事件时,返回 true 非常重要,这样就不会发生其他处理。但是,当您不处理事件时返回 false 也同样重要,这样您的应用就不会从系统的其余部分窃取关键事件。
2–21。解除软键盘
问题
您需要用户界面上的一个事件来隐藏或消除屏幕上的软键盘。
解
(API 三级)
使用InputMethodManager.hideSoftInputFromWindow()
方法明确告诉输入法管理器隐藏任何可见的输入法。
它是如何工作的
下面是一个如何在View.OnClickListener
中调用这个方法的例子:
public void onClick(View view) { InputMethodManager imm = (InputMethodManager)getSystemService( Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(view.getWindowToken(), 0); }
注意hideSoftInputFromWindow()
将 IBinder 窗口令牌作为参数。这可以通过View.getWindowToken()
从当前附加到窗口的任何视图对象中检索。在大多数情况下,特定事件的回调方法要么引用正在进行编辑的 TextView,要么引用被点击以生成事件的视图(如按钮)。这些视图是获取窗口令牌并将其传递给InputMethodManager
的最方便的对象。
2 至 22 日。自定义适配器视图空视图
问题
当 AdapterView (ListView、GridView 等)有空数据集时,您希望显示自定义视图。
解决方案
(API 一级)
将您希望显示的视图布置在 AdapterView 所在的树中,并调用AdapterView.setEmptyView()
让 AdapterView 管理它。AdapterView 将根据附加的 ListAdapter 的isEmpty()
方法的结果在它自己和它的空视图之间切换可见性参数。
**重要提示:**确保在布局中包含 AdapterView 和空视图。AdapterView 仅更改两个对象的可见性参数;它不会在布局树中插入或删除它们。
它是如何工作的
下面是用一个简单的文本视图作为空的。首先,一个包括两个视图的布局,如清单 2–49 所示。
清单 2–49。 包含 AdapterView 和一个空视图的布局
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:id="@+id/myempty" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="No Items to Display" /> <ListView android:id="@+id/mylist" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </FrameLayout>
然后,在活动中,给 ListView 一个对空视图的引用,这样它就可以被管理了(参见清单 2–50)。
清单 2–50。 连接空视图和列表的活动
`public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ListView list = (ListView)findViewById(R.id.mylist);
TextView empty = (TextView)findViewById(R.id.myempty);
//Attach the reference
list.setEmptyView(empty);
//Continue adding adapters and data to the list
}`
让空虚变得有趣
空视图不必像单一的文本视图那样简单和乏味。让我们试着让事情对用户更有用一点,并在列表为空时添加一个刷新按钮(参见清单 2–51)。
清单 2–51。 交互空布局
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <LinearLayout android:id="@+id/myempty" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="No Items to Display" /> <Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Tap Here to Refresh" /> </LinearLayout> <ListView android:id="@+id/mylist" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </FrameLayout>
现在,使用与前面相同的活动代码,我们已经将整个布局设置为空视图,并为用户添加了处理数据缺失的功能。
2–23。自定义 ListView 行
问题
您的应用需要为 ListView 中的每一行使用更加定制的外观。
解决方案
(API 一级)
创建一个定制的 XML 布局,并将其传递给一个公共适配器,或者扩展您自己的适配器。然后,您可以应用自定义状态绘图来覆盖每一行的背景和选定状态。
它是如何工作的
单纯的习俗
如果您的需求很简单,请创建一个可以连接到现有 ListAdapter 以进行填充的布局。我们将以 ArrayAdapter 为例。ArrayAdapter 可以接受要扩展的自定义布局资源的参数,以及该布局中要用数据填充的一个 TextView 的 ID。让我们为背景和满足这些要求的布局创建一些自定义的 drawables(参见清单 2–52 到 2–54)。
**清单 2–52。**RES/drawable/row _ background _ default . XML
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradient android:startColor="#EFEFEF" android:endColor="#989898" android:type="linear" android:angle="270" /> </shape>
**清单 2–53。**RES/drawable/row _ background _ pressed . XML
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <gradient android:startColor="#0B8CF2" android:endColor="#0661E5" android:type="linear" android:angle="270" /> </shape>
**清单 2–54。**RES/drawable/row _ background . XML
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true" android:drawable="@drawable/row_background_pressed"/> <item android:drawable="@drawable/row_background_default"/> </selector>
清单 2–55 显示了一个自定义布局,其中文本完全居中,而不是向左对齐。
**清单 2–55。**RES/layout/custom _ row . XML
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" android:padding="10dip" android:background="@drawable/row_background"> <TextView android:id="@+id/line1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" /> </LinearLayout>
此布局将自定义渐变状态列表集作为其背景;为列表中的每个项目设置默认状态和按下状态。现在,既然我们已经定义了一个与 ArrayAdapter 所期望的相匹配的布局,我们可以创建一个并在我们的列表中设置它,而不需要任何进一步的定制(参见清单 2–56)。
清单 2–56。 活动使用自定义行布局
`public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ListView list = new ListView(this);
ArrayAdapter adapter = new ArrayAdapter(this,
R.layout.custom_row,
R.id.line1,
new String[] {"Bill","Tom","Sally","Jenny"});
list.setAdapter(adapter);
setContentView(list);
}`
适应更复杂的选择
有时定制列表行也意味着扩展 ListAdapter。如果在一行中有多条数据,或者其中任何一条都不是文本,通常会出现这种情况。在这个例子中,让我们再次使用自定义 drawables 作为背景,但是让布局更有趣一些(参见清单 2–57)。
**清单 2–57。**RES/layout/custom _ row . XML 修改后
`
`
这种布局包含相同的居中 TextView,但每边都以 ImageView 为边界。为了将这种布局应用到 ListView,我们需要在 SDK 中扩展一个 ListAdapters。扩展哪一个取决于列表中显示的数据源。如果数据仍然只是一个简单的字符串数组,并且扩展 ArrayAdapter 就足够了。如果数据更复杂,可能需要对抽象 BaseAdapter 进行全面扩展。唯一需要扩展的方法是getView()
,它控制列表中每一行的显示方式。
在我们的例子中,数据是一个简单的字符串数组,所以我们将创建一个 ArrayAdapter 的简单扩展(参见清单 2–58)。
清单 2–58。 活动和自定义 ListAdapter 来显示新的布局
`public class MyActivity extends Activity {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ListView list = new ListView(this);
setContentView(list);
CustomAdapter adapter = new CustomAdapter(this,
R.layout.custom_row,
R.id.line1,
new String[] {"Bill","Tom","Sally","Jenny"});
list.setAdapter(adapter);
}
privateclass CustomAdapter extends ArrayAdapter {
public CustomAdapter(Context context, int layout, int resId, String[] items) {
//Call through to ArrayAdapter implementation
super(context, layout, resId, items);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View row = convertView;
//Inflate a new row if one isn’t recycled
if(row == null) {
row = getLayoutInflater().inflate(R.layout.custom_row, parent, false);
}
String item = getItem(position);
ImageView left = (ImageView)row.findViewById(R.id.leftimage);
ImageView right = (ImageView)row.findViewById(R.id.rightimage);
TextView text = (TextView)row.findViewById(R.id.line1);
left.setImageResource(R.drawable.icon);
right.setImageResource(R.drawable.icon);
text.setText(item);
return row;
}
}
}`
注意,我们使用相同的构造函数来创建适配器的实例,因为它是从ArrayAdapter
继承的。因为我们覆盖了适配器的视图显示机制,所以现在将R.layout.custom_row
和R.id.line1
传递给构造函数的唯一原因是它们是构造函数的必需参数;在这个例子中,它们不再有用了。
现在,当 ListView 想要显示一行时,它将调用其适配器上的getView()
,这是我们定制的,因此我们可以控制每行如何返回。向getView()
方法传递一个名为 convertView 的参数,这对性能非常重要。XML 的布局膨胀是一个昂贵的过程,为了最小化它对系统的影响,ListView
在列表滚动时回收视图。如果一个回收的视图可供重用,它将作为 convertView 传递到getView()
中。尽可能重用这些视图,而不是增加新的视图,以保持列表的滚动性能快速响应。
在本例中,调用getItem()
获取列表中该位置的当前值(我们的字符串数组),然后在TextView
上为该行设置该值。我们还可以将每行中的图像设置为对数据有意义的内容,尽管为了简单起见,这里将它们设置为应用图标。
2–24。制作 ListView 节标题
问题
您希望创建一个包含多个节的列表,每个节的顶部都有一个标题。
解决方案
(API 一级)
使用这里定义的 SimplerExpandableListAdapter 代码和一个ExpandableListView
。Android 没有正式的可扩展方法来创建列表中的部分,但是它提供了ExpandableListView
小部件和相关的适配器,用于处理分段列表中的二维数据结构。缺点是 SDK 提供的处理这些数据的适配器对于简单的数据结构来说很麻烦。
它是如何工作的
输入 SimplerExpandableListAdapter(参见清单 2–59),它是 BaseExpandableListAdapter 的一个扩展,作为一个例子,它处理一个Array
字符串数组,其中一个单独的字符串数组用于部分标题。
清单 2–59。 ??【SimplerExpandableListAdapter】
`public class SimplerExpandableListAdapter extends BaseExpandableListAdapter {
private Context mContext;
private String[][] mContents;
private String[] mTitles;
public SimplerExpandableListAdapter(Context context, String[] titles, String[][] contents) {
super();
//Check arguments
if(titles.length != contents.length) {
thrownew IllegalArgumentException("Titles and Contents must be the same size.");
}
mContext = context;
mContents = contents;
mTitles = titles;
}
//Return a child item
@Override
public String getChild(int groupPosition, int childPosition) {
return mContents[groupPosition][childPosition];
}
//Return a item's id
@Override
public long getChildId(int groupPosition, int childPosition) {
return 0;
}
//Return view for each item row
@Override
public View getChildView(int groupPosition, int childPosition,
boolean isLastChild, View convertView, ViewGroup parent) {
TextView row = (TextView)convertView;
if(row == null) {
row = new TextView(mContext);
}
row.setText(mContents[groupPosition][childPosition]);
return row;
}
//Return number of items in each section
@Override
public int getChildrenCount(int groupPosition) {
return mContents[groupPosition].length;
}
//Return sections
@Override
public String[] getGroup(int groupPosition) {
return mContents[groupPosition];
}
//Return the number of sections
@Override
public int getGroupCount() {
return mContents.length;
}
//Return a section's id
@Override
public long getGroupId(int groupPosition) {
return 0;
}
//Return a view for each section header
@Override
public View getGroupView(int groupPosition, boolean isExpanded,
View convertView, ViewGroup parent) {
TextView row = (TextView)convertView;
if(row == null) {
row = new TextView(mContext);
}
row.setTypeface(Typeface.DEFAULT_BOLD);
row.setText(mTitles[groupPosition]);
return row;
}
@Override
public boolean hasStableIds() {
returnfalse;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
returntrue;
}
}`
现在我们可以创建一个简单的数据结构,并用它来填充一个示例活动中的ExpandableListView
(参见清单 2–60)。
清单 2–60。 使用 SImplerExpandableListAdapter 的活动
`public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Set up an expandable list
ExpandableListView list = new ExpandableListView(this);
list.setGroupIndicator(null);
list.setChildIndicator(null);
//Set up simple data and the new adapter
String[] titles = {"Fruits","Vegetables","Meats"};
String[] fruits = {"Apples","Oranges"};
String[] veggies = {"Carrots","Peas","Broccoli"};
String[] meats = {"Pork","Chicken"};
String[][] contents = {fruits,veggies,meats};
SimplerExpandableListAdapter adapter = new SimplerExpandableListAdapter(this, titles, contents);
list.setAdapter(adapter);
setContentView(list);
}`
那该死的扩张
以这种方式使用 ExpandableListView 有一个问题:它会扩展。ExpandableListView 设计用于在点击标题时展开和折叠组标题下的子数据。此外,默认情况下,所有组都是折叠的,因此您只能看到标题项。
在某些情况下,这可能是理想的行为,但如果您只想添加节标题,通常就不是这样了。在这种情况下,需要采取两个额外的步骤:
在活动代码中,展开所有组。类似于for(int i=0; i < adapter.getGroupCount(); i++) { list.expandGroup(i); }
在适配器中,重写 onGroupCollapsed()以强制重新扩展。这将需要向适配器添加对列表小部件的引用。@Override public void onGroupCollapsed(int groupPosition) { list.expandGroup(groupPosition); }
2–25 岁。创建复合控件
问题
您需要创建一个自定义小部件,它是现有元素的集合。
解决方案
(API 一级)
通过扩展通用视图组和添加功能来创建自定义小部件。创建自定义或可重用用户界面元素的最简单、最强大的方法之一是利用 Android SDK 提供的现有小部件创建复合控件。
它是如何工作的
ViewGroup
及其子类LinearLayout
、RelativeLayout
等等,通过帮助您放置组件,为您提供了简化这一过程的工具,因此您可以更加关注添加的功能。
文字影像按钮
让我们通过制作一个 Android SDK 本身没有的小部件来创建一个例子:一个包含图像或文本作为其内容的按钮。为此,我们将创建 TextImageButton 类,它是FrameLayout
的扩展。它将包含一个用于处理文本内容的TextView
,以及一个用于图像内容的ImageView
(参见清单 2–61)。
清单 2–61。 自定义 TextImageButton 小工具
`public class TextImageButton extends FrameLayout {
private ImageView imageView;
private TextView textView;
/* Constructors */
public TextImageButton(Context context) {
this(context, null);
}
public TextImageButton(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TextImageButton(Context context, AttributeSet attrs, int defaultStyle) {
super(context, attrs, defaultStyle);
imageView = new ImageView(context, attrs, defaultStyle);
textView = new TextView(context, attrs, defaultStyle);
//create layout parameters
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
//Add the views
this.addView(imageView, params);
this.addView(textView, params);
//Make this view interactive
setClickable(true);
setFocusable(true);
//Set the default system button background
setBackgroundResource(android.R.drawable.btn_default);
//If image is present, switch to image mode
if(imageView.getDrawable() != null) {
textView.setVisibility(View.GONE);
imageView.setVisibility(View.VISIBLE);
} else {
textView.setVisibility(View.VISIBLE);
imageView.setVisibility(View.GONE);
}
}
/* Accessors */
public void setText(CharSequence text) {
//Switch to text
textView.setVisibility(View.VISIBLE);
imageView.setVisibility(View.GONE);
//Apply text
textView.setText(text);
}
public void setImageResource(int resId) {
//Switch to image
textView.setVisibility(View.GONE);
imageView.setVisibility(View.VISIBLE);
//Apply image
imageView.setImageResource(resId);
}
public void setImageDrawable(Drawable drawable) {
//Switch to image
textView.setVisibility(View.GONE);
imageView.setVisibility(View.VISIBLE);
//Apply image
imageView.setImageDrawable(drawable);
}
}`
SDK 中的所有小部件都有三个构造函数。第一个构造函数只将上下文作为参数,通常用于在代码中创建新视图。剩下的两个在从 XML 展开视图时使用,其中 XML 文件中定义的属性作为 AttributeSet 参数传入。在这里,我们使用 Java 的this()
符号将前两个构造函数深化到真正完成所有工作的那个。以这种方式构建自定义控件可以确保我们仍然可以在 XML 布局中定义该视图。如果不实现属性化的构造函数,这是不可能的。
构造函数创建一个TextView
和ImageView
,并将它们放入布局中。默认情况下,FrameLayout 不是一个交互式视图,因此构造函数使控件可点击和可聚焦,以便它可以处理用户交互事件;我们还在视图上设置了系统的默认按钮背景,以提示用户这个小部件是交互式的。剩下的代码根据作为属性传入的数据设置默认显示模式(文本或图像)。
添加访问器函数是为了方便以后切换按钮内容。如果内容发生变化,这些函数还负责在文本和图像模式之间进行切换。
因为这个自定义控件不在android.view
或android.widget
包中,所以当它在 XML 布局中使用时,我们必须使用完全限定名。清单 2–62 和 2–63 展示了一个显示定制小部件的示例活动。
清单 2–62。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <com.examples.customwidgets.TextImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="#000" android:text="Click Me!" /> <com.examples.customwidgets.TextImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/icon" /> </LinearLayout>
清单 2–63。 活动使用新的自定义小部件
`public class MyActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}`
请注意,我们仍然可以使用传统的属性来定义要显示的文本或图像等属性。这是因为我们用属性化的构造函数来构造每个项目(FrameLayout、TextView 和 ImageView ),所以每个视图设置它感兴趣的参数,而忽略其余的。
如果我们定义一个活动来使用这个布局,结果看起来像 Figure 2–16。
图 2–16。 以文本和图像两种模式显示的 TextImageButton】
需要了解的有用工具:DroidDraw
第一章介绍了一款名为UC
的单位换算安卓应用。除了探索UC
的源代码,本章还探索了这个应用的资源,从描述应用主屏幕布局的main.xml
布局文件开始。
即使对于高级开发人员来说,手工编写布局和其他资源文件也是一件乏味的事情。为此,Brendan Burns 教授创造了一个名为 DroidDraw 的工具。
DroidDraw 是一个基于 Java 的工具,它有助于构建 Android 应用的用户界面。此工具不生成应用逻辑。相反,它生成 XML 布局和其他资源信息,这些信息可以合并到另一个开发工具的应用项目中。
获取并启动 DroidDraw
DroidDraw 由droiddraw.org
网站托管。从这个网站的主页上,您可以试用 DroidDraw 作为一个 Java 小程序,也可以下载适用于 Mac OS X、Windows 和 Linux 平台的 DroidDraw 应用。
例如,点击主页面的 Windows 链接并下载droiddraw-r1b18.zip
以获得 DroidDraw for Windows。(发行版 1,Build 18 是撰写本文时最新的 DroidDraw 版本。)
解压缩droiddraw-r1b18.zip
,你会发现用于启动 DroidDraw 的droiddraw.exe
和droiddraw.jar
(一个可执行的 JAR 文件)。从 Windows 资源管理器中,双击任一文件名启动该工具。
**提示:**指定java -jar droiddraw.jar
通过 JAR 文件在命令行启动 DroidDraw。
图 2–17 展示了 DroidDraw 的用户界面。
图 2–17。 DroidDraw 的用户界面展示了一个 Android 设备屏幕的模型。
探索 DroidDraw 的用户界面
图 2–17 展示了一个简单的用户界面,包括一个菜单栏、一个屏幕区域、一个选项卡区域和一个输出区域。您可以少量拖动每个区域的边框来放大或缩小该区域。
菜单栏由文件、编辑、属性和帮助菜单组成。文件显示以下菜单项:
打开 :打开一个安卓布局文件(如main.xml
)
保存 :将当前布局信息保存到上次打开的布局文件中。如果没有打开布局文件,将显示一个对话框。
另存为 :显示一个对话框,提示用户输入布局文件名,并将当前布局信息保存到该文件中。
退出 :退出 DroidDraw。未保存的更改将会丢失。
“编辑”菜单提供以下菜单项:
剪切 :从输出区域中删除所选文本及其右侧的字符。
复制 :将选择的文本从输出区复制到剪贴板。
粘贴 :将剪贴板的内容粘贴到当前选择的内容上,或者粘贴到输出区域的当前插入符号位置。
全选 :选择输出区的全部内容。
清除屏幕 :从屏幕区域显示的用户界面中移除所有的小工具和布局信息。
从标签 设置 id:不是将文本"@+id/widget29"
分配给小部件的android:id
XML 属性,而是将小部件的值(如按钮的 OK 文本)分配给android:id
;例如"@+id/Ok"
。下次生成 XML 布局信息时,该文本将显示在输出区域中。
与文件和编辑菜单不同,项目菜单的菜单项似乎没有完全实现。
帮助菜单提供以下菜单项:
教程 :将默认浏览器指向http://www.droiddraw.org/tutorial.html
,探索一些有趣的 DroidDraw 教程。
关于 :呈现一个简单的关于对话框,没有任何版本信息。
捐赠 :将默认浏览器指向 PayPal 网站进行捐赠,以支持 DroidDraw 的持续开发。
屏幕区域呈现正在构建的 Android 屏幕的视觉反馈。它还提供了根布局和屏幕尺寸下拉列表框,用于选择哪个布局作为最终的父布局(选项包括 AbsoluteLayout、LinearLayout、RelativeLayout、ScrollView 和 TableLayout),以及选择目标屏幕尺寸,以便您知道用户界面在该屏幕上显示时的外观(选项包括 QVGA 横向、QVGA 纵向、HVGA 横向和 HVGA 纵向)。
选项卡式区域提供了一个小部件选项卡,其小部件可以被拖动到屏幕上;一个布局选项卡,其布局可以被拖动到屏幕上;一个属性选项卡,用于输入所选小部件/布局的属性值;一个字符串/颜色/数组选项卡,用于输入这些资源;以及一个支持选项卡,用于进行捐赠。
最后,输出区域提供了一个 textarea,当您单击它的 Generate 按钮时,它会显示所显示屏幕的 XML 等价物。Load 按钮似乎没有完成任何有用的事情(尽管它似乎可以撤销一个清除屏幕的操作)。
创建简单的屏幕
假设您正在构建一个应用,它显示(通过 textview 组件)一个随机选择的著名语录来响应按钮点击。你决定使用 DroidDraw 来构建应用的单一屏幕。
启动 DroidDraw,将 HVGA 肖像作为屏幕大小,并将 AbsoluteLayout 替换为 LinearLayout 作为根布局,以便在垂直列中显示 textview 和 button 组件。
**注:**与 Android 选择水平作为LinearLayout
的默认方向不同,DroidDraw 选择垂直作为默认方向。
在 Widgets 选项卡上,选择 TextView 并将其拖到屏幕上。选择属性选项卡,在宽度文本字段中输入fill_parent
,在高度文本字段中输入100px
,在文本文本字段中输入Quotation
。单击应用;图 2–18 显示了结果屏幕。
图 2–18。 文本视图组件出现在屏幕顶部。
在 Widgets 选项卡上,选择按钮并将其拖动到屏幕上。选择属性选项卡,在宽度文本字段中输入fill_parent
,在文本文本字段中输入Get Quote
。单击应用;图 2–19 显示了结果屏幕。
图 2–19。 按钮组件出现在 textview 组件下面。
从文件菜单中选择另存为,将该屏幕的 XML 保存到名为main.xml
的资源文件中。正如你在第一章的中了解到的,这个文件最终被放在一个 Android 项目的res
目录的layout
子目录中。
或者,您可以单击 Generate 按钮(在输出区域的底部)来生成屏幕的 XML(参见清单 2–64),选择该文本(通过 Edit 的 Select All 菜单项),并将其复制到剪贴板(通过 Edit 的 copy 菜单项)以备后用。
清单 2–64。 main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:id="@+id/widget27" android:layout_width="fill_parent" android:layout_height="fill_parent" xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" > <TextView android:id="@+id/widget29" android:layout_width="fill_parent" android:layout_height="100px" android:text="Quotation" > </TextView> <Button android:id="@+id/widget30" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Get Quote" > </Button> </LinearLayout>
DroidDraw 将文本分配给 XML 属性,而不是使用资源引用。例如,清单 2–64 将“Quotation”
而不是@string/quotation
分配给TextView
元素的android:text
属性。
尽管从维护的角度来看嵌入字符串是不方便的,但是您可以使用 strings 选项卡输入字符串资源名称/值对,然后单击 Save 按钮将这些资源保存到一个strings.xml
文件中,并在以后手动输入引用。
总结
正如你所看到的,Android 在提供的 SDK 中提供了一些非常灵活和可扩展的用户界面工具。正确利用这些工具意味着你可以不用担心你的应用在今天运行 Android 的各种设备上看起来和感觉上是否一样。
在这一章中,我们探讨了如何使用 Android 的资源框架为多个设备提供资源。您看到了处理静态图像以及创建自己的可绘制图像的技术。我们研究了如何覆盖窗口装饰的默认行为以及系统输入方法。我们研究了通过动画视图增加用户价值的方法。最后,我们通过创建新的定制控件和定制用于显示数据集的 AdapterViews 来扩展默认工具包。
在下一章中,我们将看看如何使用 SDK 与外界交流;访问网络资源并与其他设备通话。
三、通信和网络
许多成功的移动应用的关键是它们与远程数据源连接和交互的能力。Web 服务和 API 在当今世界非常丰富,允许应用与任何服务进行交互,从天气预报到个人财务信息。将这些数据放在用户手中,并使其可以从任何地方访问,这是移动平台的最大优势之一。Android 建立在谷歌所熟知的网络基础之上,为与外界交流提供了丰富的工具集。
3–1。显示 Web 信息
问题
来自 Web 的 HTML 或图像数据需要在应用中呈现,无需任何修改或处理。
解决方案
(API 一级)
在WebView
中显示信息。WebView
是一个视图小部件,可以嵌入到任何布局中,在您的应用中显示本地和远程的 Web 内容。WebView
基于与 Android 浏览器应用相同的开源 WebKit 技术;为应用提供相同级别的功能和能力。
它是如何工作的
在显示从网上下载的资源时,WebView
有一些非常令人满意的特性,尤其是二维滚动(同时水平和垂直于)和缩放控件。一个WebView
可以是存放大图像的完美地方,比如一个体育场地图,用户可能想要平移和缩放。在这里,我们将讨论如何使用本地和远程素材来实现这一点。
显示网址
最简单的情况是通过向WebView
提供资源的 URL 来显示 HTML 页面或图像。以下是这种技术在您的应用中的一些实际用途:
无需离开应用即可访问您的公司网站
显示 web 服务器上的实时内容页面,如 FAQ 部分,无需升级应用即可更改。
显示用户希望使用平移/缩放进行交互的大图像资源。
让我们来看一个简单的例子,它加载了一个非常流行的网页,但是在一个活动的内容视图中,而不是打开浏览器(参见清单 3–1 和 3–2)。
清单 3–1。 包含 WebView 的活动
`public class MyActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WebView webview = new WebView(this);
//Enable JavaScript support
webview.getSettings().setJavaScriptEnabled(true);
webview.loadUrl("http://www.google.com/");
setContentView(webview);
}
}`
**注意:**默认情况下,WebView
禁用 JavaScript 支持。如果您正在显示的内容需要 JavaScript,请确保在WebView.WebSettings
对象中启用它。
清单 3–2。 AndroidManifest.xml 设置所需权限
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.examples.webview" android:versionCode="1" android:versionName="1.0"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".MyActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter>
</activity> </application> <uses-permission android:name="android.permission.INTERNET" /> </manifest>
**重要提示:**如果你加载到WebView
的内容是远程的,AndroidManifest.xml 必须声明它使用了android.permission.INTERNET
权限。
结果显示您的活动中的 HTML 页面(参见 Figure 3–1)。
图 3–1。 网页视图中的 HTML 页面
本地素材
WebView
在显示本地内容时也非常有用,可以利用 HTML/CSS 格式或它为内容提供的平移/缩放行为。您可以使用 Android 项目的assets
目录来存储您希望在WebView
中显示的资源,比如大图像或 HTML 文件。为了更好地组织素材,您还可以在素材下创建目录来存储文件。
WebView.loadUrl()
可以显示使用 file:///android_asset/ <资源路径> URL schema 下存储的素材。例如,如果文件android.jpg
被放在素材目录中,那么可以使用
file:///android_asset/android.jpg
如果同样的文件放在 assets 下名为images
的目录中,WebView
可以用 URL 加载它
file:///android_assimg/android.jpg
另外,WebView.loadData()
会将存储在字符串资源或变量中的原始 HTML 加载到视图中。使用这种技术,预先格式化的 HTML 文本可以存储在res/values/strings.xml
中,或者从远程 API 下载并显示在应用中。
清单 3–3 和 3–4 展示了一个示例活动,其中两个WebView
小部件相互垂直堆叠。上面的视图显示了存储在素材目录中的一个大图像文件,下面的视图显示了存储在应用字符串资源中的一个 HTML 字符串。
清单 3–3。 res/layout/main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <WebView android:id="@+id/upperview" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1" /> <WebView android:id="@+id/lowerview" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1" /> </LinearLayout>
清单 3–4。 显示本地网页内容的活动
`public class MyActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
WebView upperView = (WebView)findViewById(R.id.upperview);
//Zoom feature must be enabled
upperView.getSettings().setBuiltInZoomControls(true);
upperView.loadUrl("file:///android_asset/android.jpg");
WebView lowerView = (WebView)findViewById(R.id.lowerview);
String htmlString =
"
Header This is HTML textFormatted in italics
";
lowerView.loadData(htmlString, "text/html", "utf-8");
}
}`
显示活动时,每个 WebView 占据屏幕垂直空间的一半。HTML 字符串按预期格式化,而大图像可以水平和垂直滚动;用户甚至可以放大或缩小(参见图 3–2)。
图 3–2。 显示本地资源的两个网页视图
3–2。拦截 WebView 事件
问题
您的应用使用 WebView 来显示内容,但也需要监听和响应用户在页面上单击的链接。
解决方案
(API 一级)
安装一个WebViewClient
并将其连接到WebView
上。WebViewClient
和WebChromeClient
是两个 WebKit 类,允许应用获得事件回调并定制WebView
的行为。默认情况下,如果没有WebViewClient
出现,WebView
会将一个 URL 传递给要处理的ActivityManager
,这通常会导致在浏览器应用中加载任何点击的链接,而不是当前的WebView
。
工作原理
在清单 3–5 中,我们创建了一个带有WebView
的活动,它将处理自己的 URL 加载。
清单 3–5。 使用 WebView 处理 URL 的活动
`public class MyActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WebView webview = new WebView(this);
webview.getSettings().setJavaScriptEnabled(true);
//Add a client to the view
webview.setWebViewClient(new WebViewClient());
webview.loadUrl("http://www.google.com");
setContentView(webview);
}
}`
在这个例子中,简单地提供一个普通的WebViewClient
到WebView
允许它自己处理任何 URL 请求,而不是把它们传递给ActivityManager
,所以点击一个链接将在同一个视图中加载所请求的页面。这是因为默认实现只是为 shouldOverrideUrlLoading()返回 false,这告诉客户端将 URL 传递给 WebView,而不是应用。
在下一个案例中,我们将利用WebViewClient.shouldOverrideUrlLoading()
回调来拦截和监控用户活动(参见清单 3–6)。
清单 3–6。 拦截 WebView URLs 的活动
`public class MyActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WebView webview = new WebView(this);
webview.getSettings().setJavaScriptEnabled(true);
//Add a client to the view
webview.setWebViewClient(mClient);
webview.loadUrl("http://www.google.com");
setContentView(webview);
}
private WebViewClient mClient = new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Uri request = Uri.parse(url);
if(TextUtils.equals(request.getAuthority(), "www.google.com")) {
//Allow the load
return false;
}
Toast.makeText(MyActivity.this, "Sorry, buddy", Toast.LENGTH_SHORT).show();
returntrue;
}
};
}`
在这个例子中,shouldOverrideUrlLoading()
根据传递的 url 决定是否将内容加载回这个WebView
中,防止用户离开 Google 的站点。返回 URL 的主机名部分,我们用它来检查用户点击的链接是否在谷歌的域名上(www.google.com)。如果我们可以验证该链接是指向另一个 Google 页面的,那么返回 false 将允许WebView
加载内容。如果没有,我们通知用户并返回 true 告诉WebViewClient
应用已经处理了这个 URL,并且不允许WebView
加载它。
这种技术可以更复杂,应用实际上通过做一些有趣的事情来处理 URL。甚至可以开发一个定制的模式来创建应用和WebView
内容之间的完整接口。
3–3 岁。使用 JavaScript 访问 WebView
问题
您的应用需要访问显示在WebView
中的当前内容的原始 HTML,以读取或修改特定的值。
解决方案
(API 一级)
创建一个 JavaScript 接口来连接WebView
和应用代码。
它是如何工作的
WebView.addJavascriptInterface()
将一个 Java 对象绑定到 JavaScript,这样就可以在WebView
中调用它的方法。使用这个接口,JavaScript 可以用来在您的应用代码和WebView
的 HTML 之间编组数据。
**注意:**允许 JavaScript 控制您的应用本身就存在安全威胁,允许远程执行应用代码。应该考虑到这种可能性来使用这个接口。
让我们来看一个实际例子。清单 3–7 展示了一个简单的 HTML 表单,它将从本地素材加载到 WebView 中。清单 3–8 是一个使用两个 JavaScript 函数在 WebView 中的活动首选项和内容之间交换数据的活动。
清单 3–7。 assets/form.html
`
`
清单 3–8。 活动与 JavaScript 桥接口
`public class MyActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
WebView webview = new WebView(this);
webview.getSettings().setJavaScriptEnabled(true);
webview.setWebViewClient(mClient);
//Attach the custom interface to the view
webview.addJavascriptInterface(new MyJavaScriptInterface(), "BRIDGE");
setContentView(webview);
//Load the form
webview.loadUrl("file:///android_asset/form.html");
}
private static final String JS_SETELEMENT = "javascript:document.getElementById('%s').value='%s'";
private static final String JS_GETELEMENT =
"javascript:window.BRIDGE.storeElement('%s',document.getElementById('%s').value)";
private static final String ELEMENTID = "emailAddress";
private WebViewClient mClient = new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//Before leaving the page, attempt to retrieve the email using JavaScript
view.loadUrl(String.format(JS_GETELEMENT, ELEMENTID, ELEMENTID));
return false;
}
@Override
public void onPageFinished(WebView view, String url) {
//When page loads, inject address into page using JavaScript
SharedPreferences prefs = getPreferences(Activity.MODE_PRIVATE);
view.loadUrl(String.format(JS_SETELEMENT, ELEMENTID,
prefs.getString(ELEMENTID, "")));
}
};
privateclass MyJavaScriptInterface {
//Store an element in preferences
@SuppressWarnings("unused")
public void storeElement(String id, String element) {
SharedPreferences.Editor edit =
getPreferences(Activity.MODE_PRIVATE).edit();
edit.putString(id, element);
edit.commit();
//If element is valid, raise a Toast
if(!TextUtils.isEmpty(element)) {
Toast.makeText(MyActivity.this, element, Toast.LENGTH_SHORT).show();
}
}
}
}`
在这个有点做作的例子中,单个元素表单是用 HTML 创建的,并显示在 WebView 中。在活动代码中,我们在 id 为“emailAddress”的WebView
中查找一个表单值,并在每次通过shouldOverrideUrlLoading()
回调点击页面上的链接(在本例中,是表单的提交按钮)时,将其值保存到SharedPreferences
。每当页面加载完成时(即onPageFinished()
被调用),我们试图将当前值从SharedPreferences
注入回web form
。
创建了一个名为MyJavaScriptInterface
的 Java 类,它定义了方法storeElement()
。当创建视图时,我们调用WebView.addJavascriptInterface()
方法将这个对象附加到视图上,并将其命名为桥。调用该方法时,String 参数是一个名称,用于引用 JavaScript 代码内部的接口。
我们在这里定义了两个 JavaScript 方法作为常量字符串,JS_GETELEMENT
和JS_SETELEMENT
。这些方法通过传递给在 WebView 上执行。loadUrl()
注意,JS_GETELEMENT
是对调用我们的自定义接口函数(引用为BRIDGE.storeElement
)的引用,该函数将调用MyJavaScripInterface
上的方法,并将表单元素的值存储在 preferences 中。如果从表单中检索到的值不为空,也会引发一个Toast
。
任何 JavaScript 都可以以这种方式在 WebView 上执行,并且它不需要作为自定义界面的一部分包含在方法中。例如,JS_SETELEMENT
使用纯 JavaScript 来设置页面上表单元素的值。
这种技术的一个流行应用是记住用户可能需要在应用中输入的表单数据,但是表单必须是基于 Web 的,例如 Web 应用的预订表单或付款表单,它没有较低级别的 API 可以访问。
3–4 岁。下载图像文件
问题
您的应用需要从 Web 或其他远程服务器下载并显示图像。
解
(API 三级)
使用AsyncTask
在后台线程中下载数据。AsyncTask
是一个包装器类,让线程化长时间运行的操作进入后台变得无痛而简单;以及用内部线程池管理并发性。除了处理后台线程之外,还在操作执行之前、期间和之后提供了回调方法,允许您在主 UI 线程上进行任何所需的更新。
它是如何工作的
在下载图像的上下文中,让我们创建一个名为 WebImageView 的 ImageView 的子类,它将从远程源缓慢加载图像,并在图像可用时立即显示。下载将在AsyncTask
操作中执行(参见清单 3–9)。
清单 3–9。 WebImageView
`public class WebImageView extends ImageView {
private Drawable mPlaceholder, mImage;
public WebImageView(Context context) {
this(context, null);
}
public WebImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WebImageView(Context context, AttributeSet attrs, int defaultStyle) {
super(context, attrs, defaultStyle);
}
public void setPlaceholderImage(Drawable drawable) {
mPlaceholder = drawable;
if(mImage == null) {
setImageDrawable(mPlaceholder);
}
}
public void setPlaceholderImage(int resid) {
mPlaceholder = getResources().getDrawable(resid);
if(mImage == null) {
setImageDrawable(mPlaceholder);
}
}
public void setImageUrl(String url) {
DownloadTask task = new DownloadTask();
task.execute(url);
}
private class DownloadTask extends AsyncTask<String, Void, Bitmap> {
@Override
protected Bitmap doInBackground(String... params) {
String url = params[0];
try {
URLConnection connection = (new URL(url)).openConnection();
InputStream is = connection.getInputStream();
BufferedInputStream bis = new BufferedInputStream(is);
ByteArrayBuffer baf = new ByteArrayBuffer(50);
int current = 0;
while ((current = bis.read()) != -1) {
baf.append((byte)current);
}
byte[] imageData = baf.toByteArray();
return BitmapFactory.decodeByteArray(imageData, 0, imageData.length);
} catch (Exception exc) {
return null;
}
}
@Override
protectedvoid onPostExecute(Bitmap result) {
mImage = new BitmapDrawable(result);
if(mImage != null) {
setImageDrawable(mImage);
}
}
};
}`
如您所见,WebImageView
是 Android ImageView
小部件的简单扩展。setPlaceholderImage()
方法允许一个本地的 drawable 被设置为显示图像,直到远程内容下载完成。一旦使用setImageUrl()
给视图一个远程 URL,大部分有趣的工作就开始了,此时定制的 AsyncTask 开始工作。
注意,AsyncTask
是强类型的,有三个输入参数值、进度值和结果值。在这种情况下,一个字符串被传递给任务的 execute 方法,后台操作应该返回一个位图。中间值,即进度,我们在这个例子中没有使用,所以它被设置为 Void。当扩展AsyncTask
时,唯一需要实现的方法是doInBackground()
,它定义了要在后台线程上运行的工作块。在前面的示例中,这是连接到所提供的远程 URL 并下载图像数据的地方。完成后,我们试图从下载的数据中创建一个Bitmap
。如果在任何一点发生错误,操作将中止并返回 null。
在AsyncTask
中定义的其他回调方法,如onPreExecute()
、onPostExecute()
和onProgressUpdate(),
在主线程上被调用,目的是更新用户界面。在前面的例子中,onPostExecute()
用于用结果数据更新视图的图像。
重要提示: Android UI 类不是线程安全的。确保使用发生在主线程上的回调方法之一来更新 UI。不要从doInBackground()
内更新视图。
清单 3–10 和 3–11 展示了一个在活动中使用这个类的简单例子。因为这个类不是android.widget
或android.view
包的一部分,所以当在 XML 中使用它时,我们必须使用完全限定的包名。
清单 3–10。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <com.examples.WebImageView android:id="@+id/webImage" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>
清单 3–11。 范例活动
`public class WebImageActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
WebImageView imageView = (WebImageView)findViewById(R.id.webImage);
imageView.setPlaceholderImage(R.drawable.icon);
imageView.setImageUrl("http://apress.com/resource/weblogo/Apress_120x90.gif");
}
}`
在这个例子中,我们首先设置一个本地图像(应用图标)作为WebImageView
占位符。该图像立即显示给用户。然后,我们告诉视图从 Web 上获取一个 press 徽标的图像。如前所述,这将在后台下载图像,并在完成后替换视图中的占位符图像。正是这种创建后台操作的简单性使得 Android 团队将AsyncTask
称为“无痛线程”。
3–5 岁。完全在后台下载
问题
应用必须下载大量资源到设备上,例如电影文件,而不需要用户保持应用活动。
解
(API 9 级)
使用DownloadManager
API。DownloadManager
是添加到 SDK 中的一项服务,API 级别为 9,允许长时间运行的下载完全由系统进行移交和管理。使用这项服务的主要优点是,DownloadManager
将继续尝试下载资源,通过失败,连接改变,甚至设备重启。
它是如何工作的
清单 3–12 是一个使用 DownloadManager 处理大型图像文件下载的示例活动。完成后,图像将显示在 ImageView 中。每当您使用 DownloadManager 从 Web 访问内容时,一定要在应用的清单中声明您正在使用android.permission.INTERNET
。
清单 3–12。 下载管理器示例活动
`public class DownloadActivity extends Activity {
private staticfinal String DL_ID = "downloadId";
private SharedPreferences prefs;
private DownloadManager dm;
private ImageView imageView;
@Override
publicvoid onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
imageView = new ImageView(this);
setContentView(imageView);
prefs = PreferenceManager.getDefaultSharedPreferences(this);
dm = (DownloadManager)getSystemService(DOWNLOAD_SERVICE);
}
@Override
publicvoid onResume() {
super.onResume();
if(!prefs.contains(DL_ID)) {
//Start the download
Uri resource = Uri.parse("http://www.bigfoto.com/dog-animal.jpg");
DownloadManager.Request request = new DownloadManager.Request(resource);
request.setAllowedNetworkTypes(Request.NETWORK_MOBILE |
Request.NETWORK_WIFI);
request.setAllowedOverRoaming(false);
//Display in the notification bar
request.setTitle("Download Sample");
long id = dm.enqueue(request);
//Save the unique id
prefs.edit().putLong(DL_ID, id).commit();
} else {
//Download already started, check status
queryDownloadStatus();
}
registerReceiver(receiver,
newIntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
@Override
publicvoid onPause() {
super.onPause();
unregisterReceiver(receiver);
}
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
publicvoid onReceive(Context context, Intent intent) {
queryDownloadStatus();
}
};
privatevoid queryDownloadStatus() {
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById(prefs.getLong(DL_ID, 0));
Cursor c = dm.query(query);
if(c.moveToFirst()) {
int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
switch(status) {
case DownloadManager.STATUS_PAUSED:
case DownloadManager.STATUS_PENDING:
case DownloadManager.STATUS_RUNNING:
//Do nothing, still in progress
break;
case DownloadManager.STATUS_SUCCESSFUL:
//Done, display the image
try {
ParcelFileDescriptor file =
dm.openDownloadedFile(prefs.getLong(DL_ID, 0));
FileInputStream fis =
new ParcelFileDescriptor.AutoCloseInputStream(file);
imageView.setImageBitmap(BitmapFactory.decodeStream(fis));
} catch (Exception e) {
e.printStackTrace();
}
break;
case DownloadManager.STATUS_FAILED:
//Clear the download and try again later
dm.remove(prefs.getLong(DL_ID, 0));
prefs.edit().clear().commit();
break;
}
}
}
}`
**重要:**截至本书出版之日,SDK 中有一个 bug 抛出异常,声称android.permission.ACCESS_ALL_DOWNLOADS
需要使用DownloadManager
。这个异常实际上是在android.permission.INTERNET
不在你的清单中时抛出的。
这个例子在Activity.onResume()
方法中完成了所有有用的工作,因此应用可以在用户每次返回活动时确定下载的状态。管理器中的下载可以通过调用DownloadManager.enqueue()
时返回的长 ID 值来引用。在本例中,我们将该值保存在应用的首选项中,以便随时监控和检索下载的内容。
在第一次启动示例应用时,会创建一个DownloadManager.Request
对象来表示要下载的内容。至少,这个请求需要远程资源的Uri
。然而,在请求上设置许多有用的属性来控制它的行为。一些有用的属性包括:
Request.setAllowedNetworkTypes()
设置可以检索下载的特定网络类型。
Request.setAllowedOverRoaming()
设置设备处于漫游连接时是否允许下载。
Request.setTitle()
为下载设置要在系统通知中显示的标题。
Request.setDescription()
为下载设置要在系统通知中显示的描述。
一旦获得了 ID,应用就使用该值来检查下载的状态。通过注册一个BroadcastReceiver
来监听ACTION_DOWNLOAD_COMPLETE
广播,应用将通过在活动的 ImageView 上设置图像文件来对下载完成做出反应。如果下载完成时活动暂停,在下次恢复时将检查状态并设置ImageView
内容。
值得注意的是,ACTION_DOWNLOAD_COMPLETE
是由DownloadManager
为它可能管理的每个下载发送的广播。因此,我们仍然需要检查我们感兴趣的下载 ID 是否真的准备好了。
目的地
在清单 3–12 的例子中,我们从未告诉 DownloadManager
将文件放在哪里。相反,当我们想要访问文件时,我们使用保存在首选项中的 ID 值的DownloadManager.openDownloadedFile()
方法来获得一个ParcelFileDescriptor
,它可以被转换成应用可以读取的流。这是获取下载内容的简单直接的方法,但是需要注意一些注意事项。
如果没有特定的目的地,文件将被下载到共享的下载缓存中,系统保留随时删除文件以回收空间的权利。因此,以这种方式下载是一种快速获取数据的便捷方式,但如果您需要更长期的下载,则应使用DownloadManager.Request
方法之一在外部存储器上指定一个永久目的地:
Request.setDestinationUri()
Request.setDestinationInExternalFilesDir()
Request.setDestinationInExternalPublicDir()
**注意:**所有写入外部存储器的目标方法都需要你的应用在清单中声明使用android.permission.WRITE_EXTERNAL_STORAGE
。
当调用DownloadManager.remove()
从管理器列表中清除条目或者用户清除下载列表时,没有明确目的地的文件也经常被删除;在这些情况下,系统不会删除下载到外部存储器的文件。
3–6 岁。访问 REST API
问题
您的应用需要通过 HTTP 访问 RESTful API,以便与远程主机的 web 服务进行交互。
解决方案
(API 三级)
在 AsyncTask 中使用 Apache HTTP 类。Android 包括 Apache HTTP 组件库,它提供了一种创建到远程 API 的连接的健壮方法。Apache 库包含一些类,可以轻松地创建 GET、POST、PUT 和 DELETE 请求,并提供对 SSL、cookie 存储、身份验证以及特定 API 在其 HttpClient 中可能具有的其他 HTTP 需求的支持。
REST 代表具象状态转移,是当今 web 服务的一种常见架构风格。RESTful APIs 通常使用标准 HTTP 动词来创建对远程资源的请求,响应通常以结构化文档格式返回,如 XML、JSON 或逗号分隔值(CSV)。
它是如何工作的
清单 3–13 是一个 AsyncTask,它可以处理任何 HttpUriRequest 并返回字符串响应。
清单 3–13。 AsyncTask 处理 HttpRequest
`public class RestTask extends AsyncTask<HttpUriRequest, Void, String> {
public static final String HTTP_RESPONSE = "httpResponse";
private Context mContext;
private HttpClient mClient;
private String mAction;
public RestTask(Context context, String action) {
mContext = context;
mAction = action;
mClient = new DefaultHttpClient();
}
public RestTask(Context context, String action, HttpClient client)
@Override
protected String doInBackground(HttpUriRequest... params) {
try{
HttpUriRequest request = params[0];
HttpResponse serverResponse = mClient.execute(request);
BasicResponseHandler handler = new BasicResponseHandler();
String response = handler.handleResponse(serverResponse);
return response;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
protectedvoid onPostExecute(String result) {
Intent intent = new Intent(mAction);
intent.putExtra(HTTP_RESPONSE, result);
//Broadcast the completion
mContext.sendBroadcast(intent);
}
}`
RestTask
可以使用或不使用 HttpClient 参数来构造。允许这样做的原因是多个请求可以使用同一个客户机对象。如果您的 API 需要 cookies 来维护一个会话,或者如果有一组特定的必需参数很容易设置一次(如 SSL 存储),这将非常有用。任务接受一个HttpUriRequest
参数进行处理(其中HttpGet
、HttpPost
、HttpPut
和HttpDelete
都是子类)并执行它。
一个BasicResponseHandler
处理响应,这是一个方便的类,它将我们的任务从需要检查响应错误中抽象出来。如果响应代码是 1XX 或 2XX,将返回字符串形式的 HTTP 响应,但是如果响应代码是 300 或更大,将抛出 HttpResponseException。
在与 API 的交互完成之后,这个类的最后一个重要部分存在于onPostExecute()
中。在构造时,RestTask 将一个字符串参数作为一个Intent
的动作,这个动作被广播回所有监听器,API 响应被封装为一个额外的。这种广播是通知 API 调用者数据已准备好进行处理的机制。
现在让我们使用这个强大的新工具来创建一些基本的 API 请求。在下面的例子中,我们使用 Yahoo!搜索 REST API。这个 API 对于每个请求只有两个必需的参数:
阿皮德
用于标识发出请求应用的唯一值
询问
表示要执行的搜索查询的字符串
访问developer.yahoo.com/search
了解更多关于这个 API 的信息。
获取示例
GET 请求是许多公共 API 中最简单也是最常见的请求。必须随请求一起发送的参数被编码到 URL 字符串本身中,因此不需要提供额外的数据。让我们创建一个 GET 请求来搜索“Android”(参见清单 3–14)。
清单 3–14。 执行 API 获取请求的活动
`public class SearchActivity extends Activity {
private static final String SEARCH_ACTION = "com.examples.rest.SEARCH";
private static final String SEARCH_URI =
"http://search.yahooapis.com/WebSearchService/V1/webSearch?appid=%s&query=%s";
private TextView result;
private ProgressDialog progress;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
result = new TextView(this);
setContentView(result);
//Create the search request
try{
String url = String.format(SEARCH_URI, "YahooDemo","Android");
HttpGet searchRequest = new HttpGet( new URI(url) );
RestTask task = new RestTask(this,SEARCH_ACTION);
task.execute(searchRequest);
//Display progress to the user
progress = ProgressDialog.show(this, "Searching", "Waiting For Results...",
true);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onResume() {
super.onResume();
registerReceiver(receiver, new IntentFilter(SEARCH_ACTION));
}
@Override
public void onPause() {
super.onPause();
unregisterReceiver(receiver);
}
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
//Clear progress indicator
if(progress != null) {
progress.dismiss();
}
String response = intent.getStringExtra(RestTask.HTTP_RESPONSE);
//Process the response data (here we just display it)
result.setText(response);
}
};
}`
在这个例子中,我们用我们想要连接的 URL 创建了我们需要的 HTTP 请求类型(在这个例子中,是对 search.yahooapis.com 的 GET 请求)。URL 存储为一个常量格式的字符串,Yahoo!API (appid 和 query)是在运行时创建请求之前添加的。
创建一个RestTask
,它带有一个独特的动作字符串,在完成时将被广播,然后任务被执行。该示例还定义了一个BroadcastReceiver
,并为发送给RestTask
的同一个动作注册了它。当任务完成时,这个接收器将捕获广播,API 响应可以被解包和处理。我们将在菜谱 3–7 和 3–8 中讨论如何解析结构化的 XML 和 JSON 响应,所以现在这个例子只是向用户界面显示原始响应。
帖子示例
很多时候,API 要求您提供一些数据作为请求的一部分,可能是认证令牌或搜索查询的内容。API 将要求您通过 HTTP POST 发送请求,因此这些值可能会被编码到请求正文中,而不是 URL 中。让我们再次运行对“Android”的搜索,但是这次使用一个帖子(参见清单 3–15)。
清单 3–15。 执行 API POST 请求的活动
`public class SearchActivity extends Activity {
private static final String SEARCH_ACTION = "com.examples.rest.SEARCH";
private static final String SEARCH_URI =
"http://search.yahooapis.com/WebSearchService/V1/webSearch";
private static final String SEARCH_QUERY = "Android";
private TextView result;
private ProgressDialog progress;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setTitle("Activity");
result = new TextView(this);
setContentView(result);
//Create the search request
try{
HttpPost searchRequest = new HttpPost( new URI(SEARCH_URI) );
List parameters = new ArrayList();
parameters.add(new BasicNameValuePair("appid","YahooDemo"));
parameters.add(new BasicNameValuePair("query",SEARCH_QUERY));
searchRequest.setEntity(new UrlEncodedFormEntity(parameters));
RestTask task = new RestTask(this,SEARCH_ACTION);
task.execute(searchRequest);
//Display progress to the user
progress = ProgressDialog.show(this, "Searching", "Waiting For Results...",
true);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onResume() {
super.onResume();
registerReceiver(receiver, new IntentFilter(SEARCH_ACTION));
}
@Override
public void onPause() {
super.onPause();
unregisterReceiver(receiver);
}`
private BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { //Clear progress indicator if(progress != null) { progress.dismiss(); } String response = intent.getStringExtra(RestTask.HTTP_RESPONSE); //Process the response data (here we just display it) result.setText(response); } }; }
注意,在这个例子中,传递给 API 执行搜索所需的参数被编码到一个HttpEntity
中,而不是直接在请求 URL 中传递。在这种情况下创建的请求是一个HttpPost
实例,它仍然是HttpUriRequest
的子类(像HttpGet
),所以我们可以使用同一个RestTask
来运行操作。与 GET 示例一样,我们将讨论解析结构化的 XML 和 JSON 响应,就像 Recipes 3–7 和 3–8 中的这个一样,所以现在这个示例只是向用户界面显示原始响应。
**注意:**Android SDK 捆绑的 Apache 库不支持多部分 HTTP POSTs。但是,来自公共可用的org.apache.http.mime
库的MultipartEntity
是兼容的,可以作为外部资源引入到您的项目中。
基本认证
使用 API 的另一个常见需求是某种形式的身份验证。针对 REST API 认证的标准正在出现,比如 OAuth 2.0,但是最常见的认证方法仍然是基于 HTTP 的基本用户名和密码认证。在清单 3–16 中,我们修改了RestTask
以支持每个请求的 HTTP 报头中的认证。
清单 3–16。 带基本认证的 rest task
`public class RestAuthTask extends AsyncTask<HttpUriRequest, Void, String> {
publicstaticfinal String HTTP_RESPONSE = "httpResponse";
private static final String AUTH_USER = "user@mydomain.com";
private static final String AUTH_PASS = "password";
private Context mContext;
private AbstractHttpClient mClient;
private String mAction;
public RestAuthTask(Context context, String action, boolean authenticate) {
mContext = context;
mAction = action;
mClient = new DefaultHttpClient();
if(authenticate) {
UsernamePasswordCredentials creds =
new UsernamePasswordCredentials(AUTH_USER, AUTH_PASS);
mClient.getCredentialsProvider().setCredentials(AuthScope.ANY, creds);
}
}
public RestAuthTask(Context context, String action, AbstractHttpClient client)
@Override
protected String doInBackground(HttpUriRequest... params) {
try{
HttpUriRequest request = params[0];
HttpResponse serverResponse = mClient.execute(request);
BasicResponseHandler handler = new BasicResponseHandler();
String response = handler.handleResponse(serverResponse);
return response;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
protectedvoid onPostExecute(String result) {
Intent intent = new Intent(mAction);
intent.putExtra(HTTP_RESPONSE, result);
//Broadcast the completion
mContext.sendBroadcast(intent);
}
}`
Apache 范例中的HttpClient
增加了基本认证。由于我们的示例任务允许传入一个特定的客户机对象以供使用,该对象可能已经具有必要的身份验证凭证,因此我们只修改了创建默认客户机的情况。在这种情况下,使用用户名和密码字符串创建一个UsernamePasswordCredentials
实例,然后在客户端的CredentialsProvider
上进行设置。
3–7 岁。解析 JSON
问题
您的应用需要解析来自 API 或其他源的响应,这些响应是用 JavaScript 对象符号(JSON)格式化的。
解
(API 一级)
使用 Android 中内置的 org.json 解析器类。SDK 附带了一组非常有效的类,用于解析 org.json 包中的 JSON 格式的字符串。只需从格式化的字符串数据中创建一个新的JSONObject
或JSONArray
,您将拥有一组访问器方法来从其中获取原始数据或嵌套的JSONObject
和JSONArray
。
它是如何工作的
默认情况下,这个 JSON 解析器是严格的,这意味着当遇到无效的 JSON 数据或无效的键时,它会异常中止。如果没有找到请求的值,以“get”为前缀的访问器方法将抛出一个JSONException
。在某些情况下,这种行为并不理想,对于,有一组附带的方法以“opt”为前缀。当找不到所请求的键值时,这些方法将返回 null,而不是抛出异常。此外,它们中的许多都有一个重载版本,该版本也接受一个 fallback 参数来返回,而不是 null。
让我们看一个如何将 JSON 字符串解析成有用片段的例子。考虑清单 3–17 中的 JSON。
清单 3–17。 例子 JSON
{ "person": { "name": "John", "age": 30, "children": [ { "name": "Billy" "age": 5 }, { "name": "Sarah" "age": 7 }, { "name": "Tommy" "age": 9 } ] } }
这用三个值定义了一个对象:name(字符串)、age(整数)和 children。名为“children”的参数是另外三个对象的数组,每个对象都有自己的名字和年龄。如果我们使用 org.json 来解析这些数据,并在 TextViews 中显示一些元素,它看起来就像清单 3–18 和清单 3–19 中的例子。
清单 3–18。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <TextView android:id="@+id/line1" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/line2" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <TextView android:id="@+id/line3" android:layout_width="fill_parent" android:layout_height="wrap_content"T /> </LinearLayout>
清单 3–19。 示例 JSON 解析活动
`public class MyActivity extends Activity {
private static final String JSON_STRING =
"{"person":{"name":"John","age":30,"children": [{"name":"Billy","age":5}," + ""name":"Sarah","age":7}, {"name":"Tommy","age":9}]}}";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
TextView line1 = (TextView)findViewById(R.id.line1);
TextView line2 = (TextView)findViewById(R.id.line2);
TextView line3 = (TextView)findViewById(R.id.line3);
try {
JSONObject person = (new JSONObject(JSON_STRING)).getJSONObject("person");
String name = person.getString("name");
line1.setText("This person's name is " + name);
line2.setText(name + " is " + person.getInt("age") + " years old.");
line3.setText(name + " has " + person.getJSONArray("children").length()
+ " children.");
} catch (JSONException e) {
e.printStackTrace();
}
}
}`
对于这个例子,JSON 字符串被硬编码为一个常量。创建活动时,字符串被转换成 JSONObject,此时它的所有数据都可以作为键-值对来访问,就像存储在地图或字典中一样。所有的业务逻辑都包装在一个 try/catch 语句中,因为我们使用严格的方法来访问数据。
函数JSONObject.getString()
、JSONObject.getInt()
用于读取原始数据并放入TextView
;getJSONArray()
方法取出嵌套的“子”数组。JSONArray
使用与JSONObject
相同的访问器方法来读取数据,但是它们将数组中的索引作为参数,而不是键的名称。此外,JSONArray
可以返回它的长度,我们在示例中使用它来显示这个人有几个孩子。
示例应用的结果如图 3–3 所示。
图 3–3。 显示活动中解析的 JSON 数据
调试窍门
JSON 是一种非常有效的符号;然而,对于人类来说,读取原始的 JSON 字符串可能很困难,这使得调试解析问题变得很困难。您正在解析的 JSON 通常来自远程数据源,或者您并不完全熟悉,出于调试目的,您需要显示它。JSONObject 和 JSONArray 都有一个重载的toString()
方法,该方法接受一个整数参数,以返回和缩进的方式漂亮地打印数据,使其更容易破译。经常在一个比较麻烦的部分加上myJsonObject.toString(2)
这样的东西,可以节省时间,也不会头疼。
3–8。解析 XML
问题
您的应用需要解析来自 API 或其他源的 XML 格式的响应。
解决方案
(API 一级)
实现org.xml.sax.helpers.DefaultHandler
的子类,使用基于事件的 SAX 解析数据。Android 有三种主要方法可以用来解析 XML 数据:DOM、SAX 和 Pull。其中实现最简单、最节省内存的是 SAX 解析器。SAX 解析通过遍历 XML 数据并在每个元素的开头和结尾生成回调事件来工作。
它是如何工作的
为了进一步描述这一点,让我们看看请求 RSS/ATOM 新闻提要时返回的 XML 格式(参见清单 3–20)。
清单 3–20。 RSS 基本结构
<rss version="2.0"> <channel> <item> <title></title> <link></link> <description></description> </item> <item> <title></title> <link></link> <description></description> </item> <item> <title></title> <link></link> <description></description> </item> … </channel> </rss>
在每个<title>
、<link>
和<description>
标签之间是与每个项目相关的值。使用 SAX,我们可以将这些数据解析成一个项目数组,然后应用可以在列表中向用户显示这些项目(参见清单 3–21)。
清单 3–21。 ??【自定义处理程序】解析 RSS
`public class RSSHandlerextends DefaultHandler {
public class NewsItem {
public String title;
public String link;
public String description;
@Override
public String toString() {
return title;
}
}
private StringBuffer buf;
private ArrayList feedItems;
private NewsItem item;
privateboolean inItem = false;
public ArrayList getParsedItems() {
return feedItems;
}
//Called at the head of each new element
@Override
public void startElement(String uri, String name, String qName, Attributes atts) {
if("channel".equals(name)) {
feedItems = new ArrayList();
} elseif("item".equals(name)) {
item = new NewsItem();
inItem = true;
} elseif("title".equals(name) && inItem) {
buf = new StringBuffer();
} elseif("link".equals(name) && inItem) {
buf = new StringBuffer();
} elseif("description".equals(name) && inItem) {
buf = new StringBuffer();
}
}
//Called at the tail of each element end
@Override
public void endElement(String uri, String name, String qName) {
if("item".equals(name)) {
feedItems.add(item);
inItem = false;
} elseif("title".equals(name) && inItem) elseif("link".equals(name) && inItem) elseif("description".equals(name) && inItem)
buf = null;
}`
//Called with character data inside elements @Override public void characters(char ch[], int start, int length) { //Don't bother if buffer isn't initialized if(buf != null) { for (int i=start; i<start+length; i++) { buf.append(ch[i]); } } } }
通过startElement()
和endElement()
在每个元素的开头和结尾通知RSSHandler
。在这两者之间,组成元素值的字符被传递到characters()
回调中。
当解析器遇到第一个元素时,条目列表被初始化。
当遇到每个 item 元素时,会初始化一个新的 NewsItem 模型。
在每个 item 元素内部,数据元素被捕获到 StringBuffer 中,并被插入到 NewsItem 的成员中。
当到达每个项目的末尾时,NewsItem 将被添加到列表中。
解析完成后,feedItems 是提要中所有项目的完整列表。
让我们通过使用 Recipe 3–6 中的 API 示例中的一些技巧来下载 RSS 格式的最新谷歌新闻(参见清单 3–22)来看看这一点。
清单 3–22。 解析 XML 并显示项目的活动
`public class FeedActivity extends Activity {
private static final String FEED_ACTION = "com.examples.rest.FEED";
private static final String FEED_URI = "http://news.google.com/?output=rss";
private ListView list;
private ArrayAdapter adapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
list = new ListView(this);
adapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1,
android.R.id.text1);
list.setAdapter(adapter);
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int position,
long id) {
NewsItem item = adapter.getItem(position);
//Launch the link in the browser
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(item.link));
startActivity(intent);
}
});
setContentView(list);
}
@Override
public void onResume() {
super.onResume();
registerReceiver(receiver, new IntentFilter(FEED_ACTION));
//Retrieve the RSS feed
try{
HttpGet feedRequest = new HttpGet( new URI(FEED_URI) );
RestTask task = new RestTask(this,FEED_ACTION);
task.execute(feedRequest);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onPause() {
super.onPause();
unregisterReceiver(receiver);
}
private BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String response = intent.getStringExtra(RestTask.HTTP_RESPONSE);
try {
//Parse the response data using SAX
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser p = factory.newSAXParser();
RSSHandler parser = new RSSHandler();
//Run the parsing operation
p.parse(new InputSource(new StringReader(response)), parser);
//Clear all current items from the list
adapter.clear();
//Add all items from the parsed XML
for(NewsItem item : parser.getParsedItems()) {
adapter.add(item);
}
//Tell adapter to update the view
adapter.notifyDataSetChanged();
} catch (Exception e) {
e.printStackTrace();
}
}
};
}`
该示例被修改为显示一个ListView
,它将由来自 RSS 提要的解析后的条目填充。在这个例子中,我们向列表中添加了一个OnItemClickListener
,它将在浏览器中启动新闻条目的链接。
一旦数据从BroadcastReceiver
中的 API 返回,Android 内置的 SAXParser 就会处理遍历 XML 字符串的工作。SAXParser.parse()
使用我们的RSSHandler
的一个实例来处理 XML,这导致处理程序的 feedItems 列表被填充。接收者然后遍历所有解析过的条目,并将它们添加到一个ArrayAdapter
中,以便在ListView
中显示。
3–8 岁。接收短信
问题
您的应用必须对传入的 SMS 消息(通常称为文本消息)做出反应。
解决方案
(API 一级)
注册一个BroadcastReceiver
来监听传入的消息,并在onReceive()
中处理它们。每当有短信传入时,操作系统就会用android.provider.Telephony.SMS_RECEIVED
动作触发一个广播意图。您的应用可以注册一个 BroadcastReceiver 来过滤这个意图并处理传入的数据。
**注意:**接收该广播并不妨碍系统的其他应用也接收它。默认的消息应用将仍然接收和显示任何传入的短信。
它是如何工作的
在之前的秘籍中,我们将BroadcastReceiver
定义为活动的私有内部成员。在这种情况下,最好单独定义接收者,并使用<receiver>
标签在 AndroidManifest.xml 中注册它。这将允许您的接收器处理传入的事件,即使您的应用是不活跃的。清单 3–23 和 3–24 显示了一个示例接收器,它监控所有收到的短信,并在一个有趣的聚会到来时举杯庆祝。
清单 3–23。 传入短信广播接收器
public class SmsReceiver extends BroadcastReceiver { private static final String SHORTCODE = "55443";
` @Override
public void onReceive(Context context, Intent intent) {
Bundle bundle = intent.getExtras();
Object[] messages = (Object[])bundle.get("pdus");
SmsMessage[] sms = new SmsMessage[messages.length];
//Create messages for each incoming PDU
for(int n=0; n < messages.length; n++) {
sms[n] = SmsMessage.createFromPdu((byte[]) messages[n]);
}
for(SmsMessage msg : sms) {
//Verify if the message came from our known sender
if(TextUtils.equals(msg.getOriginatingAddress(), SHORTCODE)) {
Toast.makeText(context,
"Received message from the mothership: "+msg.getMessageBody(),
Toast.LENGTH_SHORT).show();
}
}
}
}`
**清单 3–24。**Partial Android manifest . XML
<?xml version="1.0" encoding="utf-8"?> <manifest …> <application …> <receiver android:name=".SmsReceiver"> <intent-filter> <action android:name="android.provider.Telephony.SMS_RECEIVED" /> </intent-filter> </receiver> </application> <uses-permission android:name="android.permission.RECEIVE_SMS" /> </manifest>
**重要提示:**接收短信需要在清单中声明android.permission.RECEIVE_SMS
权限!
传入的 SMS 消息通过广播意图的附加内容作为字节数组的对象数组来传递,每个字节数组代表一个 SMS 分组数据单元(PDU)。SmsMessage.createFromPdu()
是一种方便的方法,允许我们从原始 PDU 数据创建SmsMessage
对象。设置工作完成后,我们可以检查每条消息,以确定是否有一些有趣的东西需要处理。在示例中,我们将每条消息的源地址与一个已知的短代码进行比较,并在短代码到达时通知用户。
在示例中启动 Toast 的地方,您可能希望向用户提供一些更有用的东西。也许 SMS 消息包含您的应用的 offer 代码,您可以启动适当的活动在应用中向用户显示该信息。
3–9。发送短信
问题
您的应用必须发出传出的 SMS 消息。
解决方案
(API 4 级)
使用SMSManager
发送文本和数据短信。SMSManager
是一个系统服务,处理发送 SMS 并向应用提供关于操作状态的反馈。SMSManager
提供使用SmsManager.sendTextMessage()
和SmsManager.sendMultipartTextMessage()
发送文本信息,或使用SmsManager.sendDataMessage()
发送数据信息的方法。这些方法中的每一个都采用 PendingIntent 参数来将发送操作的状态和消息传递传递回请求的目的地。
它是如何工作的
让我们来看一个简单的示例活动,它发送 SMS 消息并监控其状态(参见清单 3–25)。
清单 3–25。 活动发送短信
`public class SmsActivity extends Activity {
private static final String SHORTCODE = "55443";
private static final String ACTION_SENT = "com.examples.sms.SENT";
private static final String ACTION_DELIVERED = "com.examples.sms.DELIVERED";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Button sendButton = new Button(this);
sendButton.setText("Hail the Mothership");
sendButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sendSMS("Beam us up!");
}
});
setContentView(sendButton);
}
privatevoid sendSMS(String message) {
PendingIntent sIntent = PendingIntent.getBroadcast(this, 0,
new Intent(ACTION_SENT), 0);
PendingIntent dIntent = PendingIntent.getBroadcast(this, 0,
new Intent(ACTION_DELIVERED), 0);
//Monitor status of the operation
registerReceiver(sent, new IntentFilter(ACTION_SENT));
registerReceiver(delivered, new IntentFilter(ACTION_DELIVERED));
//Send the message
SmsManager manager = SmsManager.getDefault();
manager.sendTextMessage(SHORTCODE, null, message, sIntent, dIntent);
}
private BroadcastReceiver sent = new BroadcastReceiver(){
@Override
public void onReceive(Context context, Intent intent) {
switch (getResultCode()){
case Activity.RESULT_OK:
//Handle sent success
break;
case SmsManager.RESULT_ERROR_GENERIC_FAILURE:
case SmsManager.RESULT_ERROR_NO_SERVICE:
case SmsManager.RESULT_ERROR_NULL_PDU:
case SmsManager.RESULT_ERROR_RADIO_OFF:
//Handle sent error
break;
}
unregisterReceiver(this);
}
};
private BroadcastReceiver delivered = new BroadcastReceiver(){
@Override
public void onReceive(Context context, Intent intent) {
switch (getResultCode()){
case Activity.RESULT_OK:
//Handle delivery success
break;
case Activity.RESULT_CANCELED:
//Handle delivery failure
break;
}
unregisterReceiver(this);
}
};
}`
**重要提示:**发送短信需要在清单中声明android.permission.SEND_SMS
权限!
在本例中,每当用户点击按钮时,就会通过SMSManager
发送一条 SMS 消息。因为SMSManager
是一个系统服务,所以必须调用静态的SMSManager.getDefault()
方法来获得对它的引用。sendTextMessage()
以目的地址(号码)、服务中心地址、消息为参数。服务中心地址应该为空,以允许SMSManager
使用系统默认值。
注册了两个BroadcastReceiver
来接收将要发送的回调意图:一个用于发送操作的状态,另一个用于交付的状态。只有当操作挂起时,才会注册接收器,一旦处理了意图,它们就会注销自己。
3–10。通过蓝牙通信
问题
您希望利用蓝牙通信在应用中的设备之间传输数据。
解决方案
(API 等级 5)
使用 API Level 5 中引入的蓝牙 API 来创建对等连接。蓝牙是一种非常流行的无线技术,如今几乎所有的移动设备都采用了这种技术。许多用户认为蓝牙是他们的移动设备与无线耳机连接或与汽车立体声系统集成的一种方式。然而,蓝牙也可以是开发者在他们的应用中创建对等连接的一种简单而有效的方式。
它是如何工作的
**重要提示:**Android 模拟器目前不支持蓝牙。为了执行本例中的代码,它必须在 Android 设备上运行。此外,为了适当地测试功能,需要两个设备同时运行应用。
蓝牙点对点
清单 3–26 到 3–28 展示了一个使用蓝牙找到附近其他用户并快速交换联系信息的例子(在本例中,只是一个电子邮件地址)。通过发现可用的“服务”并通过引用其唯一的 128 位 UUID 值连接到这些服务,从而通过蓝牙建立连接。这意味着您想要使用的服务的 UUID 必须提前被发现或知道。
在本例中,同一应用在连接两端的两台设备上运行,因此我们可以自由地在代码中将 UUID 定义为常数,因为两台设备都将引用它。
**注意:**为了确保您选择的 UUID 是独一无二的,请使用网上众多免费 UUID 生成器中的一个。
**清单 3–26。**Android manifest . XML
`
<application android:icon="@drawable/icon" android:label="@string/app_name"
`
**重要提示:**记住android.permission.BLUETOOTH
必须在清单中声明才能使用这些 API。此外,必须声明android.permission.BLUETOOTH_ADMIN
,以便对首选项(如可发现性)进行更改,并启用/禁用适配器。
清单 3–27。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:id="@+id/label" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textAppearance="?android:attr/textAppearanceLarge" android:text="Enter Your Email:" /> <EditText android:id="@+id/emailField" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_below="@id/label" android:singleLine="true" android:inputType="textEmailAddress" /> <Button android:id="@+id/scanButton" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:text="Connect and Share" /> <Button
android:id="@+id/listenButton" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_above="@id/scanButton" android:text="Listen for Sharers" /> </RelativeLayout>
本例的用户界面由一个供用户输入电子邮件地址的EditText
和两个启动通信的按钮组成。标题为“监听共享者”的按钮将设备置于监听模式。在这种模式下,设备将接受任何试图与之连接的设备并与之通信。标题为“连接和共享”的按钮将设备置于搜索模式。在这种模式下,设备搜索当前正在监听的任何设备并建立连接(参见清单 3–28)。
清单 3–28。 蓝牙交流活动
`public classExchangeActivity extends Activity {
// Unique UUID for this application (generated from the web)
private static final UUID MY_UUID =
UUID.fromString("321cb8fa-9066-4f58-935e-ef55d1ae06ec");
//Friendly name to match while discovering
private static final String SEARCH_NAME = "bluetooth.recipe";
BluetoothAdapter mBtAdapter;
BluetoothSocket mBtSocket;
Button listenButton, scanButton;
EditText emailField;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
setContentView(R.layout.main);
//Check the system status
mBtAdapter = BluetoothAdapter.getDefaultAdapter();
if(mBtAdapter == null) {
Toast.makeText(this, "Bluetooth is not supported.", Toast.LENGTH_SHORT).show();
finish();
return;
}
if (!mBtAdapter.isEnabled()) {
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableIntent, REQUEST_ENABLE);
}
emailField = (EditText)findViewById(R.id.emailField);
listenButton = (Button)findViewById(R.id.listenButton);
listenButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//Make sure the device is discoverable first
if (mBtAdapter.getScanMode() !=
BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
Intent discoverableIntent = new
Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
discoverableIntent.putExtra(BluetoothAdapter. EXTRA_DISCOVERABLE_DURATION, 300);
startActivityForResult(discoverableIntent, REQUEST_DISCOVERABLE);
return;
}
startListening();
}
});
scanButton = (Button)findViewById(R.id.scanButton);
scanButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mBtAdapter.startDiscovery();
setProgressBarIndeterminateVisibility(true);
}
});
}
@Override
public void onResume() {
super.onResume();
//Register the activity for broadcast intents
IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_FOUND);
registerReceiver(mReceiver, filter);
filter = new IntentFilter(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
registerReceiver(mReceiver, filter);
}
@Override
public void onPause() {
super.onPause();
unregisterReceiver(mReceiver);
}
@Override
public void onDestroy() {
super.onDestroy();
try {
if(mBtSocket != null) {
mBtSocket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static final int REQUEST_ENABLE = 1;
private static final int REQUEST_DISCOVERABLE = 2;
@Override
protectedvoid onActivityResult(int requestCode, int resultCode, Intent data) {
switch(requestCode) {
case REQUEST_ENABLE:
if(resultCode != Activity.RESULT_OK) {
Toast.makeText(this, "Bluetooth Not Enabled.", Toast.LENGTH_SHORT).show();
finish();
}
break;
case REQUEST_DISCOVERABLE:
if(resultCode == Activity.RESULT_CANCELED) {
Toast.makeText(this, "Must be discoverable.",
Toast.LENGTH_SHORT).show();
} else {
startListening();
}
break;
default:
break;
}
}
//Start a server socket and listen
privatevoid startListening() {
AcceptTask task = new AcceptTask();
task.execute(MY_UUID);
setProgressBarIndeterminateVisibility(true);
}
//AsyncTask to accept incoming connections
privateclass AcceptTask extends AsyncTask<UUID,Void,BluetoothSocket> {
@Override
protected BluetoothSocket doInBackground(UUID... params) {
String name = mBtAdapter.getName();
try {
//While listening, set the discovery name to a specific value
mBtAdapter.setName(SEARCH_NAME);
BluetoothServerSocket socket =
mBtAdapter.listenUsingRfcommWithServiceRecord("BluetoothRecipe", params[0]);
BluetoothSocket connected = socket.accept();
//Reset the BT adapter name
mBtAdapter.setName(name);
return connected;
} catch (IOException e) {
e.printStackTrace();
mBtAdapter.setName(name);
return null;
}
}
@Override
protectedvoid onPostExecute(BluetoothSocket socket) {
if(socket == null) {
return;
}
mBtSocket = socket;
ConnectedTask task = new ConnectedTask();
task.execute(mBtSocket);
}`
`}
//AsyncTask to receive a single line of data and post
privateclass ConnectedTask extends AsyncTask<BluetoothSocket,Void,String> {
@Override
protected String doInBackground(BluetoothSocket... params) {
InputStream in = null;
OutputStream out = null;
try {
//Send your data
out = params[0].getOutputStream();
out.write(emailField.getText().toString().getBytes());
//Receive the other's data
in = params[0].getInputStream();
byte[] buffer = newbyte[1024];
in.read(buffer);
//Create a clean string from results
String result = new String(buffer);
//Close the connection
mBtSocket.close();
return result.trim();
} catch (Exception exc) {
return null;
}
}
@Override
protectedvoid onPostExecute(String result) {
Toast.makeText(ExchangeActivity.this, result, Toast.LENGTH_SHORT).show();
setProgressBarIndeterminateVisibility(false);
}
}
// The BroadcastReceiver that listens for discovered devices
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// When discovery finds a device
if (BluetoothDevice.ACTION_FOUND.equals(action)) {
// Get the BluetoothDevice object from the Intent
BluetoothDevice device =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
if(TextUtils.equals(device.getName(), SEARCH_NAME)) {
//Matching device found, connect
mBtAdapter.cancelDiscovery();
try {
mBtSocket = device.createRfcommSocketToServiceRecord(MY_UUID);
mBtSocket.connect();
ConnectedTask task = new ConnectedTask();
task.execute(mBtSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
//When discovery is complete
} elseif (BluetoothAdapter.ACTION_DISCOVERY_FINISHED.equals(action)) {
setProgressBarIndeterminateVisibility(false);
}
}
};
}`
当应用首次启动时,它会对设备的蓝牙状态进行一些基本的检查。如果BluetoothAdapter.getDefaultAdapter()
返回 null,则表明设备不支持蓝牙,应用将不再运行。即使设备上有蓝牙,应用也必须启用蓝牙才能使用它。如果蓝牙被禁用,启用适配器的首选方法是向系统发送一个意图,并以BluetoothAdapter.ACTION_REQUEST_ENABLE
作为操作。这将通知用户问题,并允许他们启用蓝牙。可以使用 enable()方法手动启用BluetoothAdapter
,但是我们强烈建议您不要这样做,除非您已经通过其他方式请求了用户的许可。
蓝牙验证后,应用等待用户输入。如前所述,该示例可以在每个设备上设置为两种模式之一,即监听模式或搜索模式。让我们看看每种模式的路径。
Listen Mode
点击“监听共享者”按钮,应用开始监听传入的连接。为了让设备接受来自它可能不知道的设备的传入连接,它必须被设定为可被发现。应用通过检查适配器的扫描模式是否等于SCAN_MODE_CONNECTABLE_DISCOVERABLE
来验证这一点。如果适配器不满足此要求,则向系统发送另一个意图,通知用户他们应该允许设备可被发现,类似于用于请求启用蓝牙的方法。如果用户接受这个请求,活动将返回一个结果,该结果等于他们允许设备被发现的时间长度;如果他们取消请求,活动将返回Activity.RESULT_CANCELED
。我们的例子监视用户在onActivityResult()
取消,并在这些条件下结束。
如果用户允许发现,或者如果设备已经被发现,则创建并执行AcceptTask
。该任务为我们定义的服务的指定 UUID 创建一个侦听器套接字,并在等待传入的连接请求时阻塞。一旦收到有效的请求,它就会被接受,应用进入连接模式。
在设备侦听期间,其蓝牙名称被设置为一个已知的唯一值(SEARCH_NAME
),以加快发现过程(我们将在“搜索模式”部分了解更多原因)。一旦建立了连接,就恢复了适配器的默认名称。
搜索模式
点击“连接和共享”按钮,让应用开始搜索要连接的另一台设备。它通过启动蓝牙发现过程并在广播接收器中处理结果来实现这一点。当通过BluetoothAdapter.startDiscovery()
开始发现时,Android 将在两种情况下通过广播异步回调:当发现另一个设备时,以及当该过程完成时。
当活动对用户可见时,私有接收器mReceiver
一直被注册,并将通过每个新发现的设备接收广播。回想一下关于监听模式的讨论,监听设备的设备名称被设置为唯一值。在每次发现时,接收器检查设备名称是否与我们已知的值匹配,并在找到一个时尝试连接。这对发现过程的速度很重要,因为否则验证每个设备的唯一方法是尝试连接到特定的服务 UUID,并查看操作是否成功。蓝牙连接过程是重量级的,而且很慢,只有在必要的时候才应该这样做,以保持事情运行良好。
这种匹配设备的方法还使用户无需手动选择要连接的设备。该应用足够智能以找到运行相同应用并处于监听模式的另一设备来完成传输。删除用户还意味着该值应该是唯一的和模糊的,以避免找到其他可能意外具有相同名称的设备。
找到匹配的设备后,我们取消发现过程(因为它也是重量级的,会降低连接速度),并连接到服务的 UUID。成功连接后,应用进入连接模式。
Connected Mode
一旦连接,两个设备上的应用将创建一个ConnectedTask
来发送和接收用户联系信息。连接的BluetoothSocket
有一个InputStream
和一个OutputStream
可用于进行数据传输。首先,电子邮件文本字段的当前值被打包并写入OutputStream
。然后,读取InputStream
以接收远程设备的信息。最后,每个设备获取它接收到的原始数据,并将其打包成一个干净的字符串显示给用户。
ConnectedTask.onPostExecute()
方法的任务是向用户显示交换的结果;目前,这是通过用接收到的内容举杯庆祝来完成的。交易完成后,连接关闭,两台设备处于相同的模式,并准备执行另一次交换。
有关这个主题的更多信息,请查看 Android SDK 提供的 BluetoothChat 示例应用。这个应用很好地演示了如何为用户在设备之间发送聊天消息建立一个长期连接。
超越安卓的蓝牙
正如我们在本节开始时提到的,除了手机和平板电脑之外,蓝牙还存在于许多无线设备中。RFCOMM 接口也存在于像蓝牙调制解调器和串行适配器这样的设备中。用于在 Android 设备之间创建对等连接的相同 API 也可以用于连接到其他嵌入式蓝牙设备,以实现监控和控制的目的。
与这些嵌入式设备建立连接的关键是获得它们支持的 RFCOMM 服务的 UUID。和前面的例子一样,通过适当的 UUID,我们可以创建一个蓝牙套接字并传输数据。然而,由于 UUID 不像上一个例子中那样为人所知,我们必须有一个发现和获得它的方法。
SDK 中有这种功能,尽管没有记录下来,并且在将来的版本中可能会有变化。
Discover a UUID
快速浏览一下 BluetoothDevice 的源代码(由于 Android 的开源根),可以发现有几个隐藏的方法可以返回远程设备的 UUID 信息。最简单的使用方法是名为getUuids()
的同步(阻塞)方法,它返回引用每个服务的ParcelUuid
对象的数组。但是,由于该方法当前是隐藏的,所以必须使用 Java 反射来调用它。下面是一个使用反射从远程设备读取服务记录的 UUIDs 的示例方法:
public ParcelUuid servicesFromDevice(BluetoothDevice device) { try { Class cl = Class.forName("android.bluetooth.BluetoothDevice"); Class[] par = {}; Method method = cl.getMethod("getUuids", par); Object[] args = {}; ParcelUuid[] retval = (ParcelUuid[])method.invoke(device, args); return retval; } catch (Exception e) { e.printStackTrace(); return null; } }
该流程还有一个名为fetchUuidsWithSdp()
的异步版本,可以以同样的方式调用。因为它是异步的,所以结果通过广播意图返回。为android.bleutooth.device.action.UUID
注册一个BroadcastReceiver
(注意 Bluetooth 的拼写错误)以获得一个带有为该设备发现的 UUIDs 的回调。获得的ParcelUuid
数组是一个额外的传递,其意图由android.bluetooth.device.extra.UUID
引用,它与同步示例的结果相同。
3–11。查询网络可达性
问题
您的应用需要知道网络连接的变化。
解决方案
(API 一级)
用ConnectivityManager
监控设备的连接性。移动应用设计中要考虑的最重要的问题之一是网络并不总是可用的。随着人们的移动,网络的速度和能力会发生变化。因此,使用网络资源的应用应该始终能够检测到这些资源是否可达,并在不可达时通知用户。
除了可达性之外,ConnectivityManager 还可以为应用提供有关连接类型的信息。这使得你可以决定是否下载一个大文件,因为用户目前正在漫游,这可能会花费他们一大笔钱。
它是如何工作的
清单 3–26 创建了一个包装器方法,您可以将它放在代码中以检查网络连接。
清单 3–29。 ConnectivityManager 包装器
public boolean isNetworkReachable() { ConnectivityManager mManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo current = mManager.getActiveNetworkInfo(); if(current == null) { return false; } return (current.getState() == NetworkInfo.State.CONNECTED); }
检查网络状态的大部分工作都是由这个包装器完成的,这个包装器方法是为了简化每次检查所有可能的网络路径。注意,如果没有可用的活动数据连接,ConnectivityManager.getActiveNetworkInfo()
将返回 null,因此我们必须首先检查这种情况。如果存在活动网络,我们可以检查其状态,这将返回以下内容之一:
不连贯的
连接
连接的
分离
当状态恢复为已连接时,网络被认为是稳定的,我们可以利用它来访问远程资源。
每当网络请求失败时,调用可达性检查,并通知用户他们的请求由于缺乏连通性而失败,这被认为是一种良好的做法。清单 3–30 是网络访问失败时这样做的一个例子。
清单 3–30。 通知用户连接失败
try { //Attempt to access network resource //May throw HttpResponseException or some other IOException on failure } catch (Exception e) { if( !isNetworkReachable() ) { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle("No Network Connection"); builder.setMessage("The Network is unavailable. Please try your request again later."); builder.setPositiveButton("OK",null); builder.create().show(); } }
确定连接类型
如果知道用户是否连接到一个对带宽收费的网络也很重要,我们可以在活动的网络连接上调用NetworkInfo.getType()
(参见清单 3–31)。
清单 3–31。 连接管理器带宽检查
public boolean isWifiReachable() { ConnectivityManager mManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo current = mManager.getActiveNetworkInfo(); if(current == null) { return false; } return (current.getType() == ConnectivityManager.TYPE_WIFI); }
这种可达性检查的修改版本确定用户是否连接到 WiFi 连接,通常指示他们在带宽不收费的情况下具有更快的连接。
总结
在当今的互联世界中,将 Android 应用连接到网络和网络服务是增加用户价值的绝佳方式。Android 用于连接网络和其他远程主机的框架使得添加这一功能变得简单明了。我们已经探索了如何将 Web 标准引入到您的应用中,使用 HTML 和 JavaScript 与用户交互,但是是在本地上下文中。您还看到了如何使用 Android 从远程服务器下载内容,并在您的应用中使用这些内容。我们还揭示了 web 服务器并不是唯一值得连接的主机,它使用蓝牙和 SMS 从一个设备直接与另一个设备通信。在下一章,我们将看看如何使用 Android 提供的工具与设备的硬件资源进行交互。
四、与设备硬件和介质交互
将应用软件与设备硬件集成为创造只有移动平台才能提供的独特用户体验提供了机会。使用麦克风和摄像头捕捉媒体允许应用通过照片或录制的问候融入个人风格。传感器和位置数据的集成可以帮助您开发应用来回答相关问题,如“我在哪里?”“我在看什么?”
在这一章中,我们将探讨如何使用 Android 提供的位置、媒体和传感器 API 来为您的应用增加移动设备带来的独特价值。
4–1。集成设备位置
问题
您希望利用设备的功能来报告其在应用中的当前物理位置。
解决方案
(API 一级)
利用 Android LocationManager
提供的后台服务。移动应用通常可以为用户提供的最强大的好处之一是能够通过包含基于用户当前位置的信息来添加上下文。应用可能会要求LocationManager
定期提供设备位置的更新,或者只是在检测到设备移动了很远的距离时才提供。
使用 Android 定位服务时,应注意尊重设备电池和用户的意愿。使用设备的 GPS 获得精细的位置定位是一个电力密集型过程,如果持续开着,会很快耗尽用户设备的电池。出于这个原因,以及其他原因,Android 允许用户禁用某些位置数据来源,如设备的 GPS。当您的应用决定如何获取位置时,必须遵守这些设置。
每个位置源也伴随着准确度的折衷。GPS 将返回更精确的位置(几米以内),但需要更长的时间来定位,并消耗更多的能量;而网络位置通常会精确到几公里,但是返回得更快并且使用更少的功率。在决定访问哪些源时,考虑应用的要求;如果您的应用只希望显示当地城市的信息,也许 GPS 定位是不必要的。
**重要提示:**在应用中使用定位服务时,请记住android.permission.ACCESS_COARSE_LOCATION
或android.permission.ACCESS_FINE_LOCATION
必须在应用清单中声明。如果你声明了android.permission.ACCESS_FINE_LOCATION
,你不需要两者,因为它也包含了粗略的权限。
它是如何工作的
在为活动或服务中的用户位置创建简单的监视器时,我们需要考虑一些操作:
确定我们要使用的源是否已启用。如果不是,决定是否要求用户启用它或尝试其他来源。
使用合理的最小距离和更新间隔值注册更新。
不再需要更新时注销更新以节省设备电量。
在清单 4–1 中,我们注册了一个活动来监听用户可见的位置更新,并在屏幕上显示该位置。
清单 4–1。 活动监控位置更新
`publicclass MyActivity extends Activity {
LocationManager manager;
Location currentLocation;
TextView locationView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
locationView = new TextView(this);
setContentView(locationView);
manager = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
}
@Override
public void onResume() {
super.onResume();
if(!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
//Ask the user to enable GPS
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Location Manager");
builder.setMessage("We want to use your location, but GPS is currently disabled.\n"
+"Would you like to change these settings now?");
builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//Launch settings, allowing user to make a change
Intent i = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
startActivity(i);
}
});
builder.setNegativeButton("No", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//No location service, no Activity
finish();
}
});
builder.create().show();
}
//Get a cached location, if it exists
currentLocation = manager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
updateDisplay();
//Register for updates
int minTime = 5000;
float minDistance = 0;
manager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
minTime, minDistance, listener);
}
@Override
public void onPause() {
super.onPause();
manager.removeUpdates(listener);
}
//Update text view
privatevoid updateDisplay() {
if(currentLocation == null) {
locationView.setText("Determining Your Location...");
} else {
locationView.setText(String.format("Your Location:\n%.2f, %.2f",
currentLocation.getLatitude(),
currentLocation.getLongitude()));
}
}
//Handle location callback events
private LocationListener listener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
currentLocation = location;
updateDisplay();
}
@Override
public void onProviderDisabled(String provider)
@Override
public void onProviderEnabled(String provider)
@Override
public void onStatusChanged(String provider, int status, Bundle extras)
};
}`
本例选择严格使用设备的 GPS 来获取位置更新。因为它是此活动功能的关键要素,所以每次恢复后承担的第一个主要任务是检查LocationManager.GPS_PROVIDER
是否仍然启用。如果出于某种原因,用户禁用了此功能,我们会询问他们是否愿意启用 GPS,让他们有机会纠正这种情况。应用没有能力为用户做到这一点,所以如果他们同意,我们使用意图动作Settings.ACTION_LOCATION_SOURCE_SETTINGS
启动一个活动,这将调出设备设置,以便用户可以启用 GPS。
一旦 GPS 处于活动状态并且可用,该活动就会注册一个LocationListener
来通知位置更新。除了提供者类型和目的地侦听器之外,LocationManager.requestLocationUpdates()
方法还接受两个重要参数:
minTime
更新之间的最小时间间隔,以毫秒为单位。
将此项设置为非零值允许位置提供者在再次更新之前休息大约指定的时间。
这是一个保存功率的参数,并且不应该被设置为任何低于最小可接受更新速率的值。
minDistance
发送下一次更新之前设备必须移动的距离,单位为米。
将此项设置为非零将阻止更新,直到确定设备至少移动了这么多。
在本例中,我们要求发送更新的频率不超过每五秒钟一次,而不考虑位置是否发生了显著变化。当这些更新到达时,注册的监听器的onLocationChanged()
方法被调用。请注意,当不同提供者的状态发生变化时,LocationListener 也会得到通知,尽管我们在这里没有利用这些回调。
**注意:**如果是在某个服务或其他后台操作中接收更新,Google 建议最小时间间隔不低于 60000(60 秒)。
该示例保存了对它接收到的最新位置的运行引用。最初,通过调用getLastKnownLocation()
将该值设置为提供者缓存的最后一个已知位置,如果提供者没有缓存的位置值,则可能返回 null。对于每个传入的更新,位置值被重置,并且用户界面显示被更新以反映新的变化。
4–2。映射位置
问题
您希望在地图上为用户显示一个或多个位置。
解决方案
(API 一级)
向用户展示地图的最简单方法是用位置数据创建一个意图,并将其传递给 Android 系统,以便在地图应用中启动。在后面的章节中,我们将更深入地研究这种方法来完成许多不同的任务。此外,可以使用 Google Maps API SDK 插件提供的MapView
和MapActivity
将地图嵌入到您的应用中。
Maps API 是核心 SDK 的附加模块,尽管它们仍然捆绑在一起。如果您还没有 Google APIs SDK,请打开 SDK 管理器,您会发现在“第三方插件”下列出了每个 API 级别的包。
为了在您的应用中使用 Maps API,必须首先从 Google 获得 API 密钥。此密钥是使用签名应用的私钥生成的。如果没有 API 键,可以使用映射类,但不会向应用返回地图切片。
**注:**欲了解关于 SDK 的更多信息,并获取 API 密钥,请访问code . Google . com/Android/add-ons/Google-APIs/mapkey . html
。还要注意,Android 对所有在调试模式下运行的应用使用相同的签名密钥(比如当它们从 IDE 中运行时),因此一个密钥可以为您在测试阶段开发的所有应用服务。
如果您在仿真器中运行代码进行测试,那么该仿真器必须使用 SDK 目标构建,该目标包括 Google APIs for mapping 以正确运行。如果从命令行创建模拟器,这些目标被命名为“Google Inc.:GoogleAPIs:X”,其中“X”是 API 版本指示器。如果您从 ide(比如 Eclipse)内部创建模拟器,那么目标具有类似的命名约定“Google API(Google Inc .)–X”,其中“X”是 API 版本指示符。
有了 API 密匙和合适的测试平台,就可以开始了。
它是如何工作的
要显示地图,只需在一个MapActivity
中创建一个MapView
的实例。在 XML 布局中,必须传递给MapView
的一个必需属性是从 Google 获得的 API 键。参见清单 4–2。
清单 4–2。 布局中的典型 MapView
<com.google.android.maps.MapView android:layout_width="fill_parent" android:layout_height="fill_parent" android:enabled="true" android:clickable="true" android:apiKey="API_KEY_STRING_HERE" />
**注意:**当将MapView
添加到 XML 布局中时,必须包括完全限定的包名,因为该类不存在于android.view
或android.widget
中。
尽管 MapView 也可以从代码中实例化,但 API 键仍然需要作为构造函数参数:
MapView map = new MapView(this, "API_KEY_STRING_HERE");
此外,应用清单必须声明它对 Maps 库的使用,Maps 库双重地充当 Android Market 过滤器,将应用从没有此功能的设备上删除。
现在,让我们来看一个例子,它将最后一个已知的用户位置放在地图上并显示出来。参见清单 4–3。
**清单 4–3。**Android manifest . XML
`
`
请注意为 INTERNET 和 ACCESS_FINE_LOCATION 声明的权限。后者是必需的,因为这个例子是挂钩回LocationManager
来获取缓存的位置值。清单中必须存在的另一个关键要素是引用 Google Maps API 的<uses-library>
标签。Android 需要这个项目来正确地将外部库链接到您的应用构建中,但它还有另一个目的。Android Market 使用库声明来过滤应用,因此它不能安装在没有配备正确映射库的设备上。参见清单 4–4。
清单 4–4。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center_horizontal" android:text="Map Of Your Location" /> <com.google.android.maps.MapView android:id="@+id/map" android:layout_width="fill_parent" android:layout_height="fill_parent" android:enabled="true" android:clickable="true" android:apiKey="YOUR_API_KEY_HERE" /> </LinearLayout>
记下您必须输入的必需 API 密钥的位置。另外,请注意,MapView
不必是活动布局中唯一的东西,尽管事实上它必须在MapActivity
中膨胀。参见清单 4–5。
清单 4–5。 显示缓存位置的地图活动
`publicclass MyActivity extends MapActivity {
MapView map;
MapController controller;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
map = (MapView)findViewById(R.id.map);
controller = map.getController();
LocationManager manager =
(LocationManager)getSystemService(Context.LOCATION_SERVICE);
Location location = manager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
int lat, lng;
if(location != null) {
//Convert to microdegrees
lat = (int)(location.getLatitude() * 1000000);
lng = (int)(location.getLongitude() * 1000000);
} else {
//Default to Google HQ
lat = 37427222;
lng = -122099167;
}
GeoPoint mapCenter = new GeoPoint(lat,lng);
controller.setCenter(mapCenter);
controller.setZoom(15);
}
//Required abstract method, return false
@Override
protectedboolean isRouteDisplayed() {
return false;
}
}`
此活动获取最新的用户位置,并将地图居中于该点。所有对地图的控制都是通过一个MapController
实例来完成的,我们通过调用MapView.getController()
来获得这个实例;控制器可用于平移、缩放和调整屏幕上的地图。在这个例子中,我们使用控制器的setCenter()
和setZoom()
方法来调整地图显示。
MapController.setCenter()
将一个GeoPoint
作为它的参数,这个参数与我们从 Android 服务接收到的Location
略有不同。主要区别在于GeoPoint
用微度(或度数* 1E6)来表示纬度和经度,而不是用表示整度的十进制值。因此,我们必须在将Location
值应用到地图之前对其进行转换。
MapController.setZoom()
允许地图以编程方式缩放到指定级别,介于 1 和 21 之间。默认情况下,地图将缩放到级别 1,SDK 文档将其定义为全局视图,每增加一个级别,地图将放大两倍。参见图 4–1。
图 4–1。 用户位置地图
您可能会注意到的第一件事是,地图没有在位置点上显示任何指示器(如大头针)。在 Recipe 4–3 中,我们将创建这些注释,并描述如何定制它们。
4–3 岁。注释地图
问题
除了显示以特定位置为中心的地图之外,您的应用还需要添加注释,以便更明显地标记该位置。
解决方案
(API 一级)
为地图创建一个自定义的ItemizedOverlay
,包括所有要标记的点。ItemizedOverlay
是一个抽象基类,它处理MapView
上各个项目的所有绘图。项目本身是OverlayItem
的实例,它是一个模型类,定义名称、副标题和可绘制标记来描述地图上的点。
它是如何工作的
让我们创建一个实现,它将获取一个 GeoPoints 数组,并使用相同的可绘制标记在地图上绘制它们。参见清单 4–6。
清单 4–6。 基本明细实现
`public class LocationOverlay extends ItemizedOverlay {
private List mItems;
public LocationOverlay(Drawable marker) {
super( boundCenterBottom(marker) );
}
public void setItems(ArrayList items) {
mItems = items;
populate();
}
@Override
protected OverlayItem createItem(int i) {
returnnew OverlayItem(mItems.get(i), null, null);
}
@Override
publicint size() {
return mItems.size();
}
@Override
protected boolean onTap(int i) {
//Handle a tap event here
return true;
}
}`
在这个实现中,构造函数使用一个Drawable
来表示放置在地图上每个位置的标记。覆盖图中使用的Drawable
必须有适当的界限,而boundCenterBottom()
是一个方便的方法来处理这个问题。具体来说,它应用了边界,使得Drawable
上接触地图位置的点将位于底部像素行的中心。
ItemizedOverlay 有两个必须被覆盖的抽象方法:createItem()
,它必须返回声明类型的对象,以及size()
,它返回被管理的项目的数量。这个例子获取了一个GeoPoint
的列表,并将它们全部包装到OverlayItem
中。一旦所有数据都出现并准备好显示,就应该在覆盖图中调用populate()
方法,在这个例子中是在setItems()
的末尾。
让我们将这个覆盖图应用到地图上,使用默认的应用图标作为标记,在 Google HQ 周围绘制三个自定义位置。参见清单 4–7。
清单 4–7。 活动使用自定义地图叠加
`public class MyActivity extends MapActivity {
MapView map;
MapController controller;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
map = (MapView)findViewById(R.id.map);
controller = map.getController();
ArrayList locations = new ArrayList();
//Google HQ @ 37.427,-122.099
locations.add(new GeoPoint(37427222,-122099167));
//Subtract 0.01 degrees
locations.add(new GeoPoint(37426222,-122089167));
//Add 0.01 degrees
locations.add(new GeoPoint(37428222,-122109167));
LocationOverlay myOverlay =
new LocationOverlay(getResources().getDrawable(R.drawable.icon));
myOverlay.setItems(locations);
map.getOverlays().add(myOverlay);
controller.setCenter(locations.get(0));
controller.setZoom(15);
}
//Required abstract method, return false
@Override
protected boolean isRouteDisplayed() {
return false;
}
}`
运行时,该活动产生如图图 4–2 所示的显示。
图 4–2。 地图与详解
请注意MapView
和ItemizedOverlay
是如何在标记上绘制阴影的。
但是,如果我们想要定制每个项目,使其显示不同的标记图像,该怎么办呢?我们该怎么做?通过显式设置项目的标记,可以为每个项目返回一个自定义的Drawable
。在这种情况下,提供给ItemizedOverlay
构造函数的 Drawable 只是一个缺省值,如果不存在自定义覆盖的话。考虑对实现进行修改,如清单 4–8 所示。
清单 4–8。 用自定义标记逐项覆盖
`public class LocationOverlay extends ItemizedOverlay {
private List mItems;
private List mMarkers;
public LocationOverlay(Drawable marker) {
super( boundCenterBottom(marker) );
}
public void setItems(ArrayList items, ArrayList drawables) {
mItems = items;
mMarkers = drawables;
populate();
}
@Override
protected OverlayItem createItem(int i) {
OverlayItem item = new OverlayItem(mItems.get(i), null, null);
item.setMarker( boundCenterBottom(mMarkers.get(i)) );
return item;
}
@Override
publicint size() {
return mItems.size();
}
@Override
protected boolean onTap(int i) {
//Handle a tap event here
return true;
}
}`
通过这一修改,创建的 OverlayItems 现在可以接收一个定制的标记图像,其形式为与图像列表中的项目索引相匹配的有界的Drawable
。如果您设置的Drawable
有状态,当选择或触摸该项目时,将显示按下和聚焦状态。我们修改后使用新实现的例子看起来像清单 4–9。
清单 4–9。 提供自定义标记的示例活动
`public class MyActivity extends MapActivity {
MapView map;
MapController controller;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
map = (MapView)findViewById(R.id.map);
controller = map.getController();
ArrayList locations = new ArrayList();
ArrayList images = new ArrayList();
//Google HQ 37.427,-122.099
locations.add(new GeoPoint(37427222,-122099167));
images.add(getResources().getDrawable(R.drawable.logo));
//Subtract 0.01 degrees
locations.add(new GeoPoint(37426222,-122089167));
images.add(getResources().getDrawable(R.drawable.icon));
//Add 0.01 degrees
locations.add(new GeoPoint(37428222,-122109167));
images.add(getResources().getDrawable(R.drawable.icon));
LocationOverlay myOverlay =
new LocationOverlay(getResources().getDrawable(R.drawable.icon));
myOverlay.setItems(locations, images);
map.getOverlays().add(myOverlay);
controller.setCenter(locations.get(0));
controller.setZoom(15);
}
//Required abstract method, return false
@Override
protected boolean isRouteDisplayed() {
return false;
}
}`
现在,我们的示例为它希望在地图上显示的每个项目提供了一个离散的图像。具体来说,我们已经决定用一个版本的 Google 徽标来代表实际的 Google HQ 位置,同时用相同的标记保留其他两个点。参见图 4–3。
图 4–3。 用自定义标记覆盖地图
让他们交互
也许您注意到了 LocationOverlay 中定义的onTap()
方法,但从未提及。ItemizedOverlay
基本实现的另一个很好的特性是,它处理点击测试,并且当它点击一个特定的项目时,有一个方便的方法来引用该项目的索引。通过这个方法,您可以敬酒、显示对话框、开始一个新的活动,或者任何其他适合用户点击注释获取更多信息的操作。
我呢?
Android 的地图 API 还包括一个特殊的覆盖图来绘制用户位置,即MyLocationOverlay
。这个覆盖图使用起来非常简单,但是只有当它所在的活动可见时才应该启用它。否则,不必要的资源使用将导致设备性能下降和电池寿命延长。参见清单 4–10。
清单 4–10。 添加 MyLocationOverlay
`public class MyActivity extends MapActivity {
MapView map;
MyLocationOverlay myOverlay;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
map = (MapView)findViewById(R.id.map);
myOverlay = new MyLocationOverlay(this, map);
map.getOverlays().add(myOverlay);
}
@Override
public void onResume() {
super.onResume();
myOverlay.enableMyLocation();
}
@Override
public void onPause() {
super.onResume();
myOverlay.disableMyLocation();
}
//Required abstract method, return false
@Override
protected boolean isRouteDisplayed() {
return false;
}
}`
这将在用户的最新位置上显示一个标准的点或箭头标记(取决于指南针是否在使用),并且只要启用覆盖,就会随着用户的移动进行跟踪。
使用MyLocationOverlay
的关键是在不使用时禁用其功能(当活动不可见时),并在需要时重新启用它们。就像使用LocationManager
一样,这确保了这些服务不会消耗不必要的能量。
4–4。捕捉图像和视频
问题
您的应用需要利用设备的摄像头来捕捉媒体,无论是静态图像还是短视频剪辑。
解决方案
(API 三级)
向 Android 发送一个意向,将控制权转移给相机应用,并返回用户捕获的图像。Android 确实包含用于直接访问相机硬件、预览和拍摄快照或视频的 API。但是,如果您的唯一目标是使用用户熟悉界面的摄像头简单地获取媒体内容,那么没有比移交更好的解决方案了。
它是如何工作的
让我们来看看如何使用相机应用拍摄静态图像和视频剪辑。
图像捕捉
让我们来看一个示例活动,当按下“拍照”按钮时,该活动将激活相机应用,并以位图的形式接收该操作的结果。参见清单 4–11 和清单 4–12。
清单 4–11。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <Button android:id="@+id/capture" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Take a Picture" /> <ImageView android:id="@+id/image" android:layout_width="fill_parent" android:layout_height="fill_parent" android:scaleType="centerInside" /> </LinearLayout>
清单 4–12。 活动捕捉图像
`public class MyActivity extends Activity {
privatestaticfinalintREQUEST_IMAGE = 100;
Button captureButton;
ImageView imageView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
captureButton = (Button)findViewById(R.id.capture);
captureButton.setOnClickListener(listener);
imageView = (ImageView)findViewById(R.id.image);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode == REQUEST_IMAGE&& resultCode == Activity.RESULT_OK) {
//Process and display the image
Bitmap userImage = (Bitmap)data.getExtras().get("data");
imageView.setImageBitmap(userImage);
}
}
private View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(intent, REQUEST_IMAGE);
}
};
}`
该方法捕获图像并返回一个缩小的位图作为“数据”字段中的额外内容。如果您需要捕获图像并需要将全尺寸图像保存在某处,在开始捕获之前,将图像目的地的Uri
插入意图的MediaStore.EXTRA_OUTPUT
字段。参见清单 4–13。
清单 4–13。 全尺寸图像捕捉到文件
`public class MyActivity extends Activity {
private static final int REQUEST_IMAGE = 100;
Button captureButton;
ImageView imageView;
File destination;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
captureButton = (Button)findViewById(R.id.capture);
captureButton.setOnClickListener(listener);
imageView = (ImageView)findViewById(R.id.image);
destination = new File(Environment.getExternalStorageDirectory(),"image.jpg");
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode == REQUEST_IMAGE&& resultCode == Activity.RESULT_OK) {
try {
FileInputStream in = new FileInputStream(destination);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 10; //Downsample by 10x
Bitmap userImage = BitmapFactory.decodeStream(in, null, options);
imageView.setImageBitmap(userImage);
} catch (Exception e) {
e.printStackTrace();
}
}
}
private View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//Add extra to save full-image somewhere
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(destination));
startActivityForResult(intent, REQUEST_IMAGE);
}
};
}`
此方法将指示相机应用将图像存储在其他地方(在本例中,在设备的 SD 卡上存储为“image.jpg”),并且结果不会按比例缩小。当操作返回后要检索图像时,我们现在直接进入我们告诉相机存储的文件位置。
然而,使用BitmapFactory.Options
,我们仍然在显示到屏幕之前缩小图像,以避免一次将全尺寸位图加载到内存中。还要注意,这个例子选择了一个位于设备外部存储器上的文件位置,这需要在 API 级别 4 及以上声明android.permission.WRITE_EXTERNAL_STORAGE
权限。如果您的最终解决方案将文件写在其他地方,这可能是不必要的。
视频拍摄
使用这种方法捕捉视频剪辑同样简单,尽管产生的结果略有不同。在任何情况下,实际的视频剪辑数据都不会直接在 Intent extras 中返回,并且总是保存到目标文件位置。以下两个参数可以作为额外参数传递:
MediaStore.EXTRA_VIDEO_QUALITY
描述用于捕获视频的质量级别的整数值。
低质量的允许值为 0,高质量的允许值为 1。
MediaStore.EXTRA_OUTPUT
保存视频内容的 Uri 目标位置。
如果不存在,视频将保存在设备的标准位置。
当视频记录完成时,数据保存的实际位置作为结果意图的数据字段中的Uri
返回。让我们看一个类似的例子,它允许用户记录并保存他们的视频,然后将保存的位置显示回屏幕。参见清单 4–14 和清单 4–15。
清单 4–14。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <Button android:id="@+id/capture" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Take a Video" /> <TextView android:id="@+id/file" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout>
清单 4–15。 活动捕捉一个视频片段
`public class MyActivity extends Activity {
private static final int REQUEST_VIDEO = 100;
Button captureButton;
TextView text;
File destination;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
captureButton = (Button)findViewById(R.id.capture);
captureButton.setOnClickListener(listener);
text = (TextView)findViewById(R.id.file);
destination = new File(Environment.getExternalStorageDirectory(),"myVideo");
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode == REQUEST_VIDEO&& resultCode == Activity.RESULT_OK) {
String location = data.getData().toString();
text.setText(location);
}
}
private View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
//Add (optional) extra to save video to our file
intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(destination));
//Optional extra to set video quality
intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
startActivityForResult(intent, REQUEST_VIDEO);
}
};
}`
这个例子和前面保存图像的例子一样,将录制的视频放在设备的 SD 卡上(对于 API 级别 4+需要android.permission.WRITE_EXTERNAL_STORAGE
权限)。为了启动这个过程,我们向媒体商店发送一个意向。ACTION_VIDEO_CAPTURE 动作字符串给系统。Android 将启动默认的相机应用来处理视频录制,并在录制完成时返回一个 OK 结果。我们通过调用onActivityResult()
回调方法中的Intent.getData()
来检索数据作为 Uri 存储的位置,然后向用户显示该位置。
此示例明确要求使用低质量设置拍摄视频,但此参数是可选的。如果MediaStore.EXTRA_VIDEO_QUALITY
不在请求意图中,设备通常会选择使用高质量拍摄。
在提供了MediaStore.EXTRA_OUTPUT
的情况下,返回的Uri
应该与您请求的位置相匹配,除非出现错误,导致应用无法写入该位置。如果不提供该参数,返回值将是一个content://Uri
,用于从系统的 MediaStore 内容提供商检索媒体。
稍后,在方法 4–8 中,我们将研究在您的应用中播放该媒体的实用方法。
4–5。制作自定相机覆盖图
问题
许多应用需要更直接地访问摄像头,或者是为了覆盖控件的自定义用户界面,或者是为了显示关于通过基于位置和方向传感器的信息可见的内容的元数据(增强现实)。
解决方案
(API 等级 5)
在自定义活动中直接连接到摄像机硬件。Android 提供 API 来直接访问设备的摄像头,以获取预览提要和拍摄照片。当应用的需求增长到不仅仅是简单地抓拍并返回一张照片以供显示时,我们可以访问这些。
**注意:**因为我们在这里对摄像机采取了更直接的方法,所以需要在清单中声明android.permission.CAMERA
权限。
它是如何工作的
我们从创建一个SurfaceView
开始,这是一个用于实时绘图的专用视图,我们将在其中附加相机的预览流。这为我们提供了一个视图中的实时预览,我们可以在活动中以我们选择的任何方式进行布局。从那以后,只需添加适合应用上下文的其他视图和控件。让我们来看看代码(参见清单 4–16 和清单 4–17)。
**注:**这里使用的Camera
级是android.hardware.Camera
,不要和android.graphics.Camera
混淆。确保在应用中导入了正确的引用。
清单 4–16。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <SurfaceView android:id="@+id/preview" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </RelativeLayout>
清单 4–17。 活动展示现场摄像预览
`import android.hardware.Camera;
publicclass PreviewActivity extends Activity implements SurfaceHolder.Callback {
Camera mCamera;
SurfaceView mPreview;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mPreview = (SurfaceView)findViewById(R.id.preview);
mPreview.getHolder().addCallback(this);
mPreview.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
mCamera = Camera.open();
}
@Override
public void onPause() {
super.onPause();
mCamera.stopPreview();
}
@Override
public void onDestroy() {
super.onDestroy();
mCamera.release();
}
//Surface Callback Methods
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
Camera.Parameters params = mCamera.getParameters();
//Get all the devices's supported sizes and pick the first (largest)
List<Camera.Size> sizes = params.getSupportedPreviewSizes();
Camera.Size selected = sizes.get(0);
params.setPreviewSize(selected.width,selected.height);
mCamera.setParameters(params);
mCamera.startPreview();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
mCamera.setPreviewDisplay(mPreview.getHolder());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder)
}`
**注意:**如果你在模拟器上测试,没有摄像头可以预览。模拟器显示什么来模拟预览取决于您运行的版本。要验证此代码是否正常工作,请在您的特定模拟器上打开 Camera 应用,并注意预览效果。这个示例中应该会出现相同的显示。
在这个例子中,我们创建了一个填充窗口的SurfaceView
,并告诉它我们的活动将被通知所有的SurfaceHolder
回调。摄像机在完全初始化之前不能在表面上显示预览信息,所以我们一直等到调用surfaceCreated()
来将视图的SurfaceHolder
附加到Camera
实例。类似地,我们等待调整预览的大小并开始绘制,直到表面被赋予其大小,这发生在调用surfaceChanged()
时。
调用Parameters.getSupportedPreviewSizes()
会返回设备可以接受的所有尺寸的列表,它们通常按从大到小的顺序排列。在本例中,我们选择第一个(因此也是最大的)预览分辨率,并用它来设置大小。
**注意:**在 2.0 (API Level 5)之前的版本中,对于Parameters.setPreviewSize()
,直接从该方法中传递高度和宽度参数是可以接受的;但在 2.0 和更高版本中,相机只会将其预览设置为设备支持的分辨率之一。否则尝试将导致异常。
Camera.startPreview()
开始在表面上实时绘制摄像机数据。请注意,预览始终以横向显示。在 Android 2.2 (API Level 8)之前,官方没有办法调整预览显示的旋转。因此,建议使用摄像机预览的活动将其方向固定为清单中的android:screenOrientation=“landscape”
以匹配。
相机服务一次只能由一个应用访问。因此,一旦不再需要摄像机,请立即致电Camera.release()
,这一点很重要。在示例中,当活动结束时,我们不再需要摄像机,因此这个调用发生在onDestroy()
中。
后来的补充
如果您的应用以它们为目标,那么在 API 的较高版本中有两个附加功能也是有用的:
Camera.setDisplayOrientation(int degrees)
API 等级 8 可用(安卓 2.2)。
将实时预览设置为 0 度、90 度、180 度或 270 度。0 映射到默认的横向方向。
Camera.open(int which)
API 级(安卓 2.3)可用。
支持多个摄像头(主要是正面和背面摄像头)。
取 0 到getNumberOfCameras()
-1 的参数。
照片覆盖
现在,我们可以在前面的示例中添加任何适合在相机预览顶部显示的控件或视图。让我们修改预览,以包括一个取消和快照照片按钮。参见清单 4–18 和清单 4–19。
清单 4–18。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <SurfaceView android:id="@+id/preview" android:layout_width="fill_parent" android:layout_height="fill_parent" /> <RelativeLayout android:layout_width="fill_parent" android:layout_height="100dip" android:layout_alignParentBottom="true" android:gravity="center_vertical" android:background="#A000"> <Button android:layout_width="100dip" android:layout_height="wrap_content" android:text="Cancel" android:onClick="onCancelClick" /> <Button android:layout_width="100dip" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:text="Snap Photo" android:onClick="onSnapClick" /> </RelativeLayout> </RelativeLayout>
清单 4–19。 添加了照片控件的活动
`public class PreviewActivity extends Activity implements
SurfaceHolder.Callback, Camera.ShutterCallback, Camera.PictureCallback {
Camera mCamera;
SurfaceView mPreview;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mPreview = (SurfaceView)findViewById(R.id.preview);
mPreview.getHolder().addCallback(this);
mPreview.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
mCamera = Camera.open();
}
@Override
public void onPause() {
super.onPause();
mCamera.stopPreview();
}
@Override
public void onDestroy() {
super.onDestroy();
mCamera.release();
Log.d("CAMERA","Destroy");
}
public void onCancelClick(View v) {
finish();
}
public void onSnapClick(View v) {
//Snap a photo
mCamera.takePicture(this, null, null, this);
}
//Camera Callback Methods
@Override
public void onShutter() {
Toast.makeText(this, "Click!", Toast.LENGTH_SHORT).show();
}
@Override
public void onPictureTaken(byte[] data, Camera camera) {
//Store the picture off somewhere
//Here, we chose to save to internal storage
try {
FileOutputStream out = openFileOutput("picture.jpg", Activity.MODE_PRIVATE);
out.write(data);
out.flush();
out.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//Must restart preview
camera.startPreview();
}
//Surface Callback Methods
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Camera.Parameters params = mCamera.getParameters();
List<Camera.Size> sizes = params.getSupportedPreviewSizes();
Camera.Size selected = sizes.get(0);
params.setPreviewSize(selected.width,selected.height);
mCamera.setParameters(params);
mCamera.setDisplayOrientation(90);
mCamera.startPreview();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
try {
mCamera.setPreviewDisplay(mPreview.getHolder());
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder)
}`
在这里,我们添加了一个简单的,部分透明的覆盖,包括一对相机操作的控制。取消所采取的行动是微不足道的;我们简单地完成活动。然而,在手动拍摄照片并将照片返回到应用时,Snap Photo 引入了更多的相机 API。一个用户动作将启动Camera.takePicture()
方法,该方法接受一系列回调指针。
注意,本例中的活动实现了另外两个接口:Camera.ShutterCallback
和Camera.PictureCallback
。前者在尽可能接近图像被捕获的时刻被调用(当“快门”关闭时),而后者可以在图像的不同形式可用的多个实例中被调用。
takePicture()的参数是单个ShutterCallback
,最多三个PictureCallback
实例。将在以下时间调用PictureCallback
(按照它们作为参数出现的顺序):
在用原始图像数据捕获图像之后
这可能会在内存有限的设备上返回 null。
在用缩放的图像数据(称为后视图像)处理图像之后
这可能会在内存有限的设备上返回 null。
在用 JPEG 图像数据压缩图像之后
这个例子只关心当 JPEG 准备好的时候被通知。因此,这也是最后一次回调,也是预览必须再次启动的时间点。如果在拍照后没有再次调用startPreview()
,那么表面上的预览将保持冻结在捕获的图像上。
4–6 岁。录制音频
问题
您有一个应用需要利用设备麦克风来记录音频输入。
解决方案
(API 一级)
使用MediaRecorder
捕捉音频并将其保存到文件中。
它是如何工作的
MediaRecorder 使用起来非常简单。您只需要提供一些关于用于编码的文件格式和数据存储位置的基本信息。清单 4–20 和 4–21 提供了一个将音频文件录制到设备的 SD 卡上的示例,用于监控用户操作的开始和停止时间。
**重要提示:**为了使用MediaRecorder
记录音频输入,您还必须在应用清单中声明android.permission.RECORD_AUDIO
权限。
清单 4–20。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <Button android:id="@+id/startButton" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Start Recording" /> <Button android:id="@+id/stopButton" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Stop Recording" android:enabled="false" /> </LinearLayout>
清单 4–21。 活动录音
`public class RecordActivity extends Activity {
private MediaRecorder recorder;
private Button start, stop;
File path;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
start = (Button)findViewById(R.id.startButton);
start.setOnClickListener(startListener);
stop = (Button)findViewById(R.id.stopButton);
stop.setOnClickListener(stopListener);
recorder = new MediaRecorder();
path = new File(Environment.getExternalStorageDirectory(),"myRecording.3gp");
resetRecorder();
}
@Override
public void onDestroy() {
super.onDestroy();
recorder.release();
}
private void resetRecorder() {
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT);
recorder.setOutputFile(path.getAbsolutePath());
try {
recorder.prepare();
} catch (Exception e) {
e.printStackTrace();
}
}
private View.OnClickListener startListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
recorder.start();
start.setEnabled(false);
stop.setEnabled(true);
} catch (Exception e) {
e.printStackTrace();
}
}
};
private View.OnClickListener stopListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
recorder.stop();
resetRecorder();
start.setEnabled(true);
stop.setEnabled(false);
}
};
}`
这个例子的用户界面非常简单。有两个按钮,用户可以根据录制状态交替使用。当用户按下 start 时,我们启用 stop 按钮并开始记录。当用户按下 stop 时,我们重新启用 start 按钮,并将记录器重置为再次运行。
MediaRecorder 的设置非常简单。我们在 SD 卡上创建一个名为“myRecording.3gp”的文件,并在setOutputFile()
中传递路径。其余的设置方法告诉录像机使用设备麦克风作为输入(音频源。MIC),并使用默认编码器为输出创建 3GP 文件格式。
现在,你可以使用任何设备的文件浏览器或媒体播放器应用来播放这个音频文件。稍后,在方法 4–8 中,我们将指出如何通过应用播放音频。
4–7 岁。添加语音识别
问题
您的应用需要语音识别技术来解释语音输入。
解决方案
(API 三级)
使用android.speech
包的类来利用每个 Android 设备的内置语音识别技术。每一个配备语音搜索的 Android 设备(从 Android 1.5 开始提供)都为应用提供了使用内置SpeechRecognizer
处理语音输入的能力。
要激活这个过程,应用只需向系统发送一个RecognizerIntent
,识别服务将记录语音输入并对其进行处理;返回一个字符串列表,表明识别器认为它听到了什么。
它是如何工作的
让我们来看看这项技术的实际应用。参见清单 4–22。
清单 4–22。 活动发起并处理语音识别
`public class RecognizeActivity extends Activity {
private static final int REQUEST_RECOGNIZE = 100;
TextView tv;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
tv = new TextView(this);
setContentView(tv);
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
intent.putExtra(RecognizerIntent.EXTRA_PROMPT, "Tell Me Your Name");
try {
startActivityForResult(intent, REQUEST_RECOGNIZE);
} catch (ActivityNotFoundException e) {
//If no recognizer exists, download one from Android Market
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Not Available");
builder.setMessage("There is currently no recognition application installed."
+" Would you like to download one?");
builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//Download, for example, Google Voice Search
Intent marketIntent = new Intent(Intent.ACTION_VIEW);
marketIntent.setData
(Uri.parse("market://details?id=com.google.android.voicesearch"));
}
});
builder.setNegativeButton("No", null);
builder.create().show();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode == REQUEST_RECOGNIZE&& resultCode == Activity.RESULT_OK) {
ArrayList matches = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
StringBuilder sb = new StringBuilder();
for(String piece : matches) {
sb.append(piece);
sb.append('\n');
}
tv.setText(sb.toString());
} else {
Toast.makeText(this, "Operation Canceled", Toast.LENGTH_SHORT).show();
}
}
}`
**注意:**如果你在模拟器中测试你的应用,要注意 Android Market 和任何语音识别器都不太可能安装。最好在设备上测试这个例子的操作。
这个例子在应用启动时自动启动语音识别活动,并要求用户“告诉我你的名字”。收到用户的语音并处理结果后,Activity 返回用户可能说过的内容列表。这个列表是按照概率排序的,所以在很多情况下,简单地称matches.get(0)
为最佳选择并继续前进是明智的。但是,该活动获取所有返回值,并出于娱乐目的将它们显示在屏幕上。
当启动SpeechRecognizer
时,有许多额外的东西可以传递,目的是定制行为。本例使用了两种最常见的方法:
额外 _ 语言 _ 模型
帮助微调来自语音处理器的结果的值。
典型的语音到文本查询应该使用 LANGUAGE_MODEL_FREE_FORM 选项。
如果进行较短的请求类型查询,LANGUAGE_MODEL_WEB_SEARCH 可能会产生更好的结果。
额外提示
除此之外,传递一些其他参数也是有用的:
额外 _ 最大 _ 结果
额外语言
请求以不同于当前系统默认语言的语言返回结果。
有效 IETF 标签的字符串值,如“en-US”或“es”
4–8 岁。播放音频/视频
问题
应用需要在设备上播放本地或远程的音频或视频内容。
解
(API 一级)
使用MediaPlayer
播放本地或流媒体。无论内容是音频还是视频,本地还是远程,MediaPlayer
都将高效地连接、准备和播放相关媒体。在这个菜谱中,我们还将探索使用MediaController
和VideoView
作为简单的方法来将交互和视频播放包含到活动布局中。
它是如何工作的
**注意:**在期望播放特定的媒体剪辑或流之前,请阅读开发者文档的“Android 支持的媒体格式”部分以验证支持。
音频播放
让我们看一个简单的例子,只用MediaPlayer
来播放声音。参见清单 4–23。
清单 4–23。 活动播放本地声音
`public class PlayActivity extends Activity implements MediaPlayer.OnCompletionListener {
Button mPlay;
MediaPlayer mPlayer;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPlay = new Button(this);
mPlay.setText("Play Sound");
mPlay.setOnClickListener(playListener);
setContentView(mPlay);
}
@Override
public void onDestroy() {
super.onDestroy();
if(mPlayer != null) {
mPlayer.release();
}
}
private View.OnClickListener playListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if(mPlayer == null) {
try {
mPlayer = MediaPlayer.create(PlayActivity.this, R.raw.sound);
mPlayer.start();
} catch (Exception e) {
e.printStackTrace();
}
} else {
mPlayer.stop();
mPlayer.release();
mPlayer = null;
}
}
};
//OnCompletionListener Methods
@Override
public void onCompletion(MediaPlayer mp) {
mPlayer.release();
mPlayer = null;
}
}`
此示例使用一个按钮来开始和停止本地声音文件的回放,该文件存储在项目的 res/raw 目录中。MediaPlayer.create()
是一种具有多种形式的便利方法,旨在一步完成玩家对象的构建和准备。本例中使用的表单引用了一个本地资源 ID,但是也可以使用create()
来访问和播放远程资源
MediaPlayer.create(Context context, Uri uri);
创建后,该示例立即开始播放声音。声音播放时,用户可以再次按下按钮停止播放。该活动还实现了MediaPlayer.OnCompletionListener
接口,因此当播放操作正常完成时,它会收到一个回调。
在这两种情况下,一旦停止播放,MediaPlayer 实例就会被释放。这种方法允许资源仅在被使用时才被保留,并且声音可以被播放多次。为了确保资源不会被不必要地保留,当活动被销毁时,如果它仍然存在,玩家也会被释放。
如果您的应用需要播放许多不同的声音,您可以考虑在播放结束时调用reset()
而不是release()
。但是记住,当玩家不再被需要的时候(或者活动结束了),还是要给release()
打电话。
音频播放器
除了简单的回放之外,如果应用需要为用户创建一种交互式体验,以便能够播放、暂停和搜索媒体,该怎么办?MediaPlayer 上有一些方法可以用自定义 UI 元素来实现所有这些功能,但是 Android 也提供了 MediaController 视图,所以您不必这么做。参见列表 4–24 和 4–25。
清单 4–24。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/root" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="Now Playing..." /> <ImageView android:id="@+id/coverImage" android:layout_width="fill_parent" android:layout_height="fill_parent" android:scaleType="centerInside" /> </LinearLayout>
清单 4–25。 用媒体控制器播放音频的活动
`public class PlayerActivity extends Activity implements
MediaController.MediaPlayerControl, MediaPlayer.OnBufferingUpdateListener {
MediaController mController;
MediaPlayer mPlayer;
ImageView coverImage;
int bufferPercent = 0;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
coverImage = (ImageView)findViewById(R.id.coverImage);
mController = new MediaController(this);
mController.setAnchorView(findViewById(R.id.root));
}
@Override
public void onResume() {
super.onResume();
mPlayer = new MediaPlayer();
//Set the audio data source
try {
mPlayer.setDataSource(this, Uri.parse("URI_TO_REMOTE_AUDIO"));
mPlayer.prepare();
} catch (Exception e) {
e.printStackTrace();
}
//Set an image for the album cover
coverImage.setImageResource(R.drawable.icon);
mController.setMediaPlayer(this);
mController.setEnabled(true);
}
@Override
public void onPause() {
super.onPause();
mPlayer.release();
mPlayer = null;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
mController.show();
return super.onTouchEvent(event);
}
//MediaPlayerControl Methods
@Override
public int getBufferPercentage() {
return bufferPercent;
}
@Override
public int getCurrentPosition() {
return mPlayer.getCurrentPosition();
}
@Override
public int getDuration() {
return mPlayer.getDuration();
}
@Override
public boolean isPlaying() {
return mPlayer.isPlaying();
}
@Override
public void pause() {
mPlayer.pause();
}
@Override
public void seekTo(int pos) {
mPlayer.seekTo(pos);
}
@Override
public void start() {
mPlayer.start();
}
//BufferUpdateListener Methods
@Override
public void onBufferingUpdate(MediaPlayer mp, int percent) {
bufferPercent = percent;
}
//Android 2.0+ Target Callbacks
public boolean canPause() {
return true;
}
public boolean canSeekBackward() {
return true;
}
public boolean canSeekForward() {
return true;
}
}`
这个例子创建了一个简单的音频播放器,它显示与正在播放的音频相关联的艺术家或封面艺术的图像(我们只是在这里将其设置为应用图标)。该示例仍然使用 MediaPlayer 实例,但是这一次我们没有使用create()
便利方法来创建它。相反,我们在创建实例后使用setDataSource()
来设置内容。当以这种方式附加内容时,播放器不会自动准备好,所以我们还必须调用prepare()
来准备好播放器以供使用。
此时,音频准备开始。我们希望MediaController
能够处理所有的回放控制,但是MediaController
只能附加到实现了MediaController.MediaPlayerControl
接口的对象上。奇怪的是,MediaPlayer
本身并没有实现这个接口,所以我们指定 Activity 来做这项工作。该接口包含的七个方法中有六个实际上是由MediaPlayer
实现的,所以我们直接调用这些方法。
**后期添加:**如果您的应用面向 API Level 5 或更高版本,那么在MediaController.MediaPlayerControl
接口中有三个额外的方法要实现:
canPause() canSeekBackward() canSeekForward()
这些方法只是告诉系统我们是否希望允许这些操作在这个控件中发生,所以我们的例子为所有三个返回true
。如果你的目标是一个较低的 API 级别,这些方法不是必需的(这就是为什么我们没有在它们上面提供@Override
注释),但是你可以在以后的版本上运行时实现它们以获得最好的结果。
需要使用MediaController
的最后一个方法是getBufferPercentage()
。为了获得这些数据,该活动还负责实现MediaPlayer.OnBufferingUpdateListener
,它会随着缓冲百分比的变化而更新。
MediaController 的实现有一个技巧。它被设计成一个小部件,在自己的窗口中浮动在一个活动视图之上,一次只能看到几秒钟。因此,我们没有在内容视图的 XML 布局中实例化小部件,而是在代码中实例化。通过调用setAnchorView()
在媒体控制器和内容视图之间建立链接,这也决定了控制器在屏幕上的显示位置。在这个例子中,我们将它锚定到根布局对象,因此它将显示在屏幕的底部。如果MediaController
锚定到层次结构中的子视图,它将显示在该子视图的旁边。
此外,由于控制器的独立窗口,不得从onCreate()
内部调用MediaController.show()
,这样做会导致致命的异常。
MediaController
设计为默认隐藏,由用户激活。在这个例子中,我们覆盖了活动的onTouchEvent()
方法,以便每当用户点击屏幕时显示控制器。除非用参数 0 调用show()
,否则它会在该参数标注的时间后淡出。在没有任何参数的情况下调用show()
告诉它在默认超时(大约三秒)后淡出。参见图 4–4。
图 4–4。 使用媒体控制器的活动
现在,音频回放的所有功能都由标准控制器小部件处理。本例中使用的版本setDataSource()
采用了一个 Uri,使得适合于从 ContentProvider 或远程位置加载音频。请记住,所有这些都可以很好地处理本地音频文件和使用备用形式的setDataSource()
的资源。
视频播放器
播放视频时,通常需要一整套播放控件来播放、暂停和查找内容。此外,MediaPlayer 必须有一个对 SurfaceHolder 的引用,它可以在该 surface holder 上绘制视频帧。正如我们在前面的例子中提到的,Android 提供 API 来完成所有这些工作,并创建自定义的视频播放体验。然而,在许多情况下,最有效的前进方式是让 SDK 提供的类,即MediaController
和VideoView
,来完成所有繁重的工作。
我们来看一个在活动中创建视频播放器的例子。参见清单 4–26。
清单 4–26。 活动播放视频内容
`public class VideoActivity extends Activity {
VideoView videoView;
MediaController controller;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
videoView = new VideoView(this);
videoView.setVideoURI( Uri.parse("URI_TO_REMOTE_VIDEO") );
controller = new MediaController(this);
videoView.setMediaController(controller);
videoView.start();
setContentView(videoView);
}
@Override
public void onDestroy() {
super.onDestroy();
videoView.stopPlayback();
}
}`
此示例将远程视频位置的 URI 传递给 VideoView,并告诉它处理其余部分。VideoView 也可以嵌入到更大的 XML 布局层次结构中,尽管它通常是惟一的东西,并且是全屏显示的,所以在代码中设置为布局树中的惟一视图并不少见。
有了VideoView
,和MediaController
的交互就简单多了。VideoView
实现了MediaController.MediaPlayerControl
接口,因此不需要额外的粘合逻辑来使控件起作用。VideoView
也在内部处理控制器到自身的锚定,所以它显示在屏幕上适当的位置。
处理重定向
关于使用 MediaPlayer 类处理远程内容,我们还有最后一点要注意。如今,网络上的许多媒体内容服务器并不公开展示视频容器的直接 URL。出于跟踪或安全的目的,公共媒体 URL 通常会在到达真正的媒体内容之前重定向一次或多次。
MediaPlayer 不处理此重定向过程,当显示重定向的 URL 时会返回错误。
如果您无法直接检索要在应用中显示的内容的位置,该应用必须在将 URL 传递给 MediaPlayer 之前跟踪重定向路径。清单 4–27 是一个简单的 AsyncTask 跟踪程序的例子。
清单 4–27。 RedirectTracerTask
`public class RedirectTracerTask extends AsyncTask<Uri, Void, Uri> {
private VideoView mVideo;
private Uri initialUri;
public RedirectTracerTask(VideoView video) {
super();
mVideo = video;
}
@Override
protected Uri doInBackground(Uri... params) {
initialUri = params[0];
String redirected = null;
try {
URL url = new URL(initialUri.toString());
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
//Once connected, see where you ended up
redirected = connection.getHeaderField("Location");
return Uri.parse(redirected);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
protected void onPostExecute(Uri result) {
if(result != null) {
mVideo.setVideoURI(result);
} else {
mVideo.setVideoURI(initialUri);
}
}
}`
这个助手类通过从 HTTP 头中检索最终位置来跟踪它。如果提供的 Uri 中没有重定向,后台操作将返回 null,在这种情况下,原始 Uri 将被传递给 VideoView。使用这个助手类,您现在可以将位置传递给视图,如下所示:
`VideoView videoView = new VideoView(this);
RedirectTracerTask task = new RedirectTracerTask(videoView);
Uri location = Uri.parse("URI_TO_REMOTE_VIDEO");
task.execute(location);`
4–9。创建倾斜监视器
问题
您的应用需要来自设备加速度计的反馈,而不仅仅是了解设备是纵向还是横向。
解决方案
(API 三级)
使用SensorManager
接收来自加速度传感器的持续反馈。SensorManager
提供一个通用抽象接口,用于在 Android 设备上使用传感器硬件。加速度计只是应用可以注册以接收定期更新的众多传感器之一。
它是如何工作的
**重要提示:**设备传感器,比如加速度计,不存在于模拟器中。如果您无法在 Android 设备上测试SensorManager
代码,您将需要使用 SensorSimulator 等工具将传感器事件注入系统。SensorSimulator 要求修改此示例以使用不同的SensorManager
接口进行测试;请参阅本章末尾的“有用的工具:传感器模拟器”了解更多信息。
该示例活动向SensorManager
注册加速度计更新,并在屏幕上显示数据。原始的 X/Y/Z 数据显示在屏幕底部的TextView
中,但此外,设备的“倾斜”通过一个简单的图形在TableLayout
中的四个视图中可视化。参见列表 4–28 和 4–29。
**注意:**我们还建议您将android:screenOrientation=“portrait”
或android:screenOrientation=“landscape”
添加到应用的清单中,以防止活动在您移动和倾斜设备时试图旋转。
清单 4–28。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TableLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:stretchColumns="0,1,2"> <TableRow android:layout_weight="1">
<View android:id="@+id/top" android:layout_column="1" /> </TableRow> <TableRow android:layout_weight="1"> <View android:id="@+id/left" android:layout_column="0" /> <View android:id="@+id/right" android:layout_column="2" /> </TableRow> <TableRow android:layout_weight="1"> <View android:id="@+id/bottom" android:layout_column="1" /> </TableRow> </TableLayout> <TextView android:id="@+id/values" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" /> </RelativeLayout>
清单 4–29。 倾斜监控活动
`public class TiltActivity extends Activity implements SensorEventListener {
private SensorManager mSensorManager;
private Sensor mAccelerometer;
private TextView valueView;
private View mTop, mBottom, mLeft, mRight;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
valueView = (TextView)findViewById(R.id.values);
mTop = findViewById(R.id.top);
mBottom = findViewById(R.id.bottom);
mLeft = findViewById(R.id.left);
mRight = findViewById(R.id.right);
}
protected void onResume() {
super.onResume();
mSensorManager.registerListener(this, mAccelerometer,
SensorManager.SENSOR_DELAY_UI);
}
protected void onPause() {
super.onPause();
mSensorManager.unregisterListener(this);
}
public void onAccuracyChanged(Sensor sensor, int accuracy)
public void onSensorChanged(SensorEvent event) {
float[] values = event.values;
float x = values[0]/10;
float y = values[1]/10;
int scaleFactor;
if(x > 0) {
scaleFactor = (int)Math.min(x*255, 255);
mRight.setBackgroundColor(Color.TRANSPARENT);
mLeft.setBackgroundColor(Color.argb(scaleFactor, 255, 0, 0));
} else {
scaleFactor = (int)Math.min(Math.abs(x)*255, 255);
mRight.setBackgroundColor(Color.argb(scaleFactor, 255, 0, 0));
mLeft.setBackgroundColor(Color.TRANSPARENT);
}
if(y > 0) {
scaleFactor = (int)Math.min(y*255, 255);
mTop.setBackgroundColor(Color.TRANSPARENT);
mBottom.setBackgroundColor(Color.argb(scaleFactor, 255, 0, 0));
} else {
scaleFactor = (int)Math.min(Math.abs(y)*255, 255);
mTop.setBackgroundColor(Color.argb(scaleFactor, 255, 0, 0));
mBottom.setBackgroundColor(Color.TRANSPARENT);
}
//Display the raw values
valueView.setText(String.format("X: %1$1.2f, Y: %2$1.2f, Z: %3$1.2f",
values[0], values[1], values[2]));
}
}`
从纵向观看设备屏幕的角度来看,设备加速计上三个轴的方向如下:
x:水平轴,正指向右侧
y:正向上的垂直轴
z:正对着你的垂直轴
当活动对用户可见时(在onResume()
和onPause()
之间),它向SensorManager
注册以接收关于加速度计的更新。注册时,registerListener()
的最后一个参数定义了更新速率。所选的值SENSOR_DELAY_UI,
是接收更新并在每次更新时直接修改用户界面的最快推荐速率。
对于每个新的传感器值,用一个SensorEvent
值调用我们注册的监听器的onSensorChanged()
方法;该事件包含 X/Y/Z 加速度值。
**快速科学笔记:**加速度计测量由于施加的力而产生的加速度。当设备处于静止状态时,唯一作用于其上的力是重力(~9.8 米/秒 2 )。每个轴上的输出值是这个力(向下指向地面)和每个方向向量的乘积。当两者平行时,该值将达到最大值(9.8-10)。当两者垂直时,该值将处于最小值(0.0)。因此,平放在桌子上的设备的 X 和 Y 读数都为 0.0,z 读数为 9.8。
示例应用在屏幕底部的 TextView 中显示每个轴的原始加速度值。此外,还有一个由四个View
组成的网格,以上/下/左/右的模式排列,我们根据方向按比例调整这个网格的背景颜色。当设备完全平坦时,X 和 Y 都应该接近零,整个屏幕将是黑色的。当设备倾斜时,倾斜位置低侧的方块将开始发出红光,直到设备方向在任何一个位置达到直立时,方块完全变成红色。
**提示:**试着用其他的比率值修改这个例子,比如SENSOR_DELAY_NORMAL
。请注意示例中的更改如何影响更新速率。
此外,您可以摇动设备,并在设备向各个方向加速时看到交替的网格框高亮显示。
4–10。监控指南针方向
问题
您的应用希望通过监控设备的指南针传感器来了解用户面对的主要方向。
解决方案
(API 三级)
再次前来救援。Android 并不完全提供“指南针”传感器,而是包括必要的方法来根据其他传感器数据收集设备指向的位置。在这种情况下,设备的磁场传感器将与加速度计结合使用,以确定用户面对的位置。
然后,我们可以使用getOrientation()
向 SensorManager 询问用户相对于地球的方位。
工作原理
**重要提示:**模拟器中不存在加速度计这样的设备传感器。如果您无法在 Android 设备上测试SensorManager
代码,您将需要使用 SensorSimulator 等工具将传感器事件注入系统。SensorSimulator 要求修改此示例以使用不同的SensorManager
接口进行测试;请参阅本章末尾的“有用的工具:传感器模拟器”了解更多信息。
与前面的加速度计示例一样,我们使用 SensorManager 注册所有感兴趣的传感器(在本例中有两个)的更新,并在onSensorChanged()
中处理结果。此示例从设备摄像头的视角计算并显示用户方向,因为这是增强现实等应用所需要的。参见列表 4–30 和 4–31。
清单 4–30。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:id="@+id/direction" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:textSize="64dip" android:textStyle="bold" /> <TextView android:id="@+id/values" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" /> </RelativeLayout>
清单 4–31。 活动监控用户定位
`public class CompassActivity extends Activity implements SensorEventListener {
private SensorManager mSensorManager;
private Sensor mAccelerometer, mField;
private TextView valueView, directionView;
privatefloat[] mGravity;
privatefloat[] mMagnetic;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mField = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
valueView = (TextView)findViewById(R.id.values);
directionView = (TextView)findViewById(R.id.direction);
}
protected void onResume() {
super.onResume();
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_UI);
mSensorManager.registerListener(this, mField, SensorManager.SENSOR_DELAY_UI);
}
protected void onPause() {
super.onPause();
mSensorManager.unregisterListener(this);
}
privatevoid updateDirection() {
float[] temp = newfloat[9];
float[] R = newfloat[9];
//Load rotation matrix into R
SensorManager.getRotationMatrix(temp, null, mGravity, mMagnetic);
//Map to camera's point-of-view
SensorManager.remapCoordinateSystem(temp, SensorManager.AXIS_X, SensorManager.AXIS_Z, R);
//Return the orientation values
float[] values = newfloat[3];
SensorManager.getOrientation(R, values);
//Convert to degrees
for (int i=0; i < values.length; i++) {
Double degrees = (values[i] * 180) / Math.PI;
values[i] = degrees.floatValue();
}
//Display the compass direction
directionView.setText( getDirectionFromDegrees(values[0]) );
//Display the raw values
valueView.setText(String.format("Azimuth: %1$1.2f, Pitch: %2$1.2f, Roll: %3$1.2f",
values[0], values[1], values[2]));
}
private String getDirectionFromDegrees(float degrees) {
if(degrees >= -22.5 && degrees < 22.5) { return "N"; }
if(degrees >= 22.5 && degrees < 67.5) { return "NE"; }
if(degrees >= 67.5 && degrees < 112.5) { return "E"; }
if(degrees >= 112.5 && degrees < 157.5) { return "SE"; }
if(degrees >= 157.5 || degrees < -157.5) { return "S"; }
if(degrees >= -157.5 && degrees < -112.5) { return "SW"; }
if(degrees >= -112.5 && degrees < -67.5) { return "W"; }
if(degrees >= -67.5 && degrees < -22.5) { return "NW"; }
return null;
}
public void onAccuracyChanged(Sensor sensor, int accuracy)
public void onSensorChanged(SensorEvent event) {
switch(event.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
mGravity = event.values.clone();
break;
case Sensor.TYPE_MAGNETIC_FIELD:
mMagnetic = event.values.clone();
break;
default:
return;
}
if(mGravity != null&& mMagnetic != null) {
updateDirection();
}
}
}`
本示例活动在屏幕底部实时显示传感器计算返回的三个原始值。此外,与用户当前面对的位置相关联的罗盘方向被转换并显示在舞台中央。当从传感器接收到更新时,维护来自每个传感器的最新值的本地副本。一旦我们从两个感兴趣的传感器收到至少一个读数,我们就允许 UI 开始更新。
所有繁重的工作都在这里进行。
SensorManager.getOrientation()
提供了我们需要的输出信息显示方向。该方法不返回任何数据,而是传入一个空的浮点数组供该方法填充三个角度值,它们表示(按顺序):
方位角
绕直接指向地球的轴的旋转角度。
这是该示例的感兴趣的值。
投
卷
传递给getOrientation()
的参数之一是一个表示旋转矩阵的浮点数组。旋转矩阵是设备的当前坐标系如何定向的表示,因此该方法可以基于其参考坐标提供适当的旋转角度。使用getRotationMatrix()
获得设备方向的旋转矩阵,该矩阵将来自加速度计和磁场传感器的最新值作为输入。和getOrientation()
一样,它也返回 void 长度为 9 或 16 的空浮点数组(表示 3×3 或 4×4 的矩阵)必须作为第一个参数传入,以便该方法填充。
最后,我们希望方向计算的输出特定于摄像机的视角。为了进一步转换获得的旋转,我们使用remapCoordinateSystem()
方法。该方法接受四个参数(按顺序):
表示要转换的矩阵的输入数组
如何相对于世界坐标转换设备的 X 轴
如何相对于世界坐标转换设备的 Y 轴
用于填充结果的空数组
在我们的示例中,我们希望 X 轴保持不变,因此我们将 X 映射到 X。但是,我们希望将设备的 Y 轴(垂直轴)与世界的 Z 轴(指向地球的轴)对齐。这将使我们接收到的旋转矩阵定向,以匹配垂直拿着的设备,就好像用户正在使用相机并在屏幕上观看预览一样。
计算出角度数据后,我们进行一些数据转换,并将结果显示在屏幕上。getOrientation()
的单位输出是弧度,所以在显示之前我们首先要把每个结果转换成度数。此外,我们需要将方位值转换为罗盘方向;getDirectionFromDegrees()
是一个助手方法,根据当前读数所在的范围返回正确的方向。顺时针旋转一整圈,从北到南的方位角读数为 0 到 180 度。继续绕着圆圈,方位角将从南到北旋转-180 到 0 度。
需要了解的有用工具:传感器模拟器
谷歌的 Android 模拟器不支持传感器,因为大多数计算机没有指南针、加速度计,甚至没有模拟器可以利用的光传感器。虽然这种限制对于需要与传感器交互的应用来说是有问题的,并且模拟器是唯一可行的测试选项,但它可以通过使用传感器模拟器来克服。
传感器模拟器 ( [
code.google.com/p/openintents/wiki/SensorSimulator](http://code.google.com/p/openintents/wiki/SensorSimulator)
)是一个开源工具,让你模拟传感器数据,并使这些数据可用于你的应用进行测试。目前支持加速度计、磁场(指南针)、方位、温度、条码阅读器传感器;这些传感器的行为可以通过各种配置设置来定制。
注意: Sensor Simulator 是由 OpenIntents ( [
code.google.com/p/openintents/wiki/OpenIntents](http://code.google.com/p/openintents/wiki/OpenIntents)
)向 Android 开发者提供的几个项目之一,这是一个由谷歌托管的为 Android 平台创建可重用组件和工具的项目。
获取传感器模拟器
传感器模拟器分布在一个单独的 ZIP 存档中。将浏览器指向[
code.google.com/p/openintents/downloads/list?q=sensorsimulator](http://code.google.com/p/openintents/downloads/list?q=sensorsimulator)
,点击sensorsimulator-1.1.0-rc1.zip
链接,然后点击下一页的sensorsimulator-1.1.0-rc1.zip
链接,下载这个 284Kb 的文件。
解压缩这个归档文件后,您会发现一个包含以下子目录的sensorsimulator-1.1.0-rc1
主目录:
**bin
😗* 包含sensorsimulator-1.1.0-rc1.jar
(让您生成测试数据的传感器模拟器独立 Java 应用)和SensorSimulatorSettings-1.1.0-rc1.apk
(设置默认 IP 地址/端口设置并测试传感器模拟器 Java 应用连接的 Android 应用)可执行文件以及这些可执行文件的自述文件。
lib
:包含sensorsimulator-lib-1.1.0-rc1.jar
库,您的 Android 应用使用该库从传感器模拟器 Java 应用访问传感器设置。
**release
😗* 包含 Apache Ant 构建脚本来组装sensorsimulator-1.1.0-rc1.zip
版本。
**samples
😗* 包含一个关于如何从 Android 应用访问传感器模拟器的SensorDemo
Android 应用示例。
**SensorSimulator
😗* 包含传感器模拟器 Java 应用的源代码。
**SensorSimulatorSettings
😗* 包含传感器模拟器设置 Android 应用的源代码和用于构建其 APK 和库文件的项目设置。
启动传感器模拟器设置和传感器模拟器
既然您已经下载并解压缩了 Sensor Simulator 发行版,那么您需要启动这个软件。完成以下步骤来完成此任务:
启动 Android 模拟器,如果还没有运行;比如在命令行执行emulator -avdtest_AVD
。这个例子假设你已经在第一章中创建了test_AVD
。
在模拟器上安装SensorSimulatorSettings-1.1.0-rc1.apk
;比如执行adb install SensorSimulatorSettings-1.1.0-rc1.apk
。这个例子假设通过您的PATH
环境变量可以访问adb
工具,并且bin
目录是最新的。当 APK 成功安装在模拟器上时,它会输出一条成功消息。
点击应用启动器屏幕的传感器模拟器图标,启动传感器模拟器应用。
启动bin
目录的传感器模拟器 Java 应用,它位于sensorsimulator-1.1.0-rc1.jar
中。例如,在 Windows 下,双击该文件名。
图 4–5 显示了模拟器的应用启动器屏幕,其中传感器模拟器图标高亮显示。
图 4–5。 传感器模拟器图标在应用启动器屏幕上高亮显示。
单击传感器模拟器图标。图 4–6 显示了分为两个活动的传感器模拟器设置屏幕:设置和测试。
图 4–6。 默认设置活动提示为 IP 地址和套接字端口。
设置活动提示您输入传感器模拟器 Java 应用的 IP 地址和套接字端口号,其用户界面显示在图 4–7 中。
图 4–7。 使用传感器模拟器应用的用户界面将传感器数据发送到传感器模拟器设置和您自己的应用。
Sensor Simulator 提供了一个选项卡式用户界面,每个选项卡都允许您将测试数据发送到不同的仿真器实例。目前,只有一个默认的传感器模拟器选项卡,但您可以添加更多的选项卡,并通过从文件菜单中选择新建选项卡和关闭选项卡菜单项来删除它们。
每个选项卡分为三个窗格:
左侧窗格显示设备的图形,该图形显示了设备的方向和位置。它还允许您选择套接字端口和 Telnet 套接字端口,显示连接信息,并且(默认情况下)仅显示加速度计、磁场和方向传感器数据。
中间窗格允许您调整设备的偏航、俯仰和滚动,选择支持哪些传感器,启用合适的传感器进行测试,并选择其他传感器数据(如选择当前温度值)以及传感器数据发送到仿真器的频率。
右侧窗格允许您通过 Telnet 与模拟器实例通信。您可以交流电池状态(例如电池是否存在以及电池的健康状况——是否过热?)连同 GPS 数据一起发送到模拟器实例。
左侧窗格显示要在设置活动的 IP 地址文本字段中输入的 IP 地址(本例中为 192.168.100.100)。因为 Sensor Simulator 使用的端口号(8010)与 Settings 活动的 Socket textfield 中显示的端口号相同,所以您不需要更改该字段的值。
**注意:**如果 8010 正被您计算机上运行的其他应用使用,您可能需要更改设置活动的套接字文本字段和传感器模拟器的套接字文本字段中的端口号。
在设置活动的 IP 地址字段中输入该 IP 地址后(参见图 4–6,点击测试选项卡选择测试活动。图 4–8 显示了结果。
图 4–8。 点击连接,连接到传感器模拟器 app,开始接收测试数据。
根据此屏幕,您必须单击 Connect 按钮来建立与 Sensor Simulator Java 应用的连接,该应用此时必须正在运行。(您稍后可以单击“断开”来断开连接。)
点按“连接”后,“测试”标签会显示加速计、磁场和方向复选框,其下方带有标签以显示测试值。它不显示温度和条形码读取器的复选框,因为这些传感器既不被支持也不被启用(参见传感器模拟器应用的中间面板)。
选中 acclerometer 复选框,如图 4–9 所示,复选框下方的标签显示从传感器模拟器获得的当前偏航、俯仰和横滚值。
图 4–9。 传感器模拟器设置应用正在从传感器模拟器应用接收加速度计数据。
从您的应用访问传感器模拟器
虽然传感器模拟器设置可以帮助您学习如何使用传感器模拟器将测试数据发送到应用,但它不能替代您自己的应用。在某种程度上,您会希望将代码合并到访问该工具的活动中。Google 为修改您的应用以访问 Sensor Simulator 提供了以下指南:
将lib
目录的 JAR 文件(例如sensorsimulator-lib-1.1.0-rc1.jar
)添加到您的项目中。
将该库中的以下传感器模拟器类型导入源代码:import org.openintents.sensorsimulator.hardware.Sensor; import org.openintents.sensorsimulator.hardware.SensorEvent; import org.openintents.sensorsimulator.hardware.SensorEventListener; import org.openintents.sensorsimulator.hardware.SensorManagerSimulator;
用等效的SensorManagerSimulator.getSystemService()
方法调用替换活动的onCreate()
方法的现有SensorManager.getSystemService()
方法调用。例如,你可以用mSensorManager = SensorManagerSimulator.getSystemService(this, SENSOR_SERVICE);
代替mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
。
例如,使用之前通过SensorSimulatorSettings
: mSensorManager.connectSimulator();
设置的设置连接到传感器模拟器 Java 应用。
所有其他代码保持不变。但是,记得在onResume()
中注册传感器,在onStop()
: @Override protected void onResume() { super.onResume(); mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER), SensorManager.SENSOR_DELAY_FASTEST); mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD), SensorManager.SENSOR_DELAY_FASTEST); mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION), SensorManager.SENSOR_DELAY_FASTEST); mSensorManager.registerListener(this, mSensorManager.getDefaultSensor(Sensor.TYPE_TEMPERATURE), SensorManager.SENSOR_DELAY_FASTEST); } @Override protected void onStop() { mSensorManager.unregisterListener(this); super.onStop(); }
中取消注册
最后,您必须实现SensorEventListener
接口:`class MySensorActivity extends Activity implements SensorEventListener
{
public void onAccuracyChanged(Sensor sensor, int accuracy)
public void onSensorChanged(SensorEvent event)
{
int sensor = event.type;
float[] values = event.values;
// do something with the sensor data
}
}`
注意: OpenIntents 的SensorManagerSimulator
类是从 Android 的SensorManager
类派生出来的,实现的功能和SensorManager
完全一样。对于回调,新的SensorEventListener
界面已经实现,类似于标准的 Android SensorEventListener
界面。
每当您没有连接到 Sensor Simulator Java 应用时,您将获得真实的设备传感器数据:org.openintents.hardware.SensorManagerSimulator
类透明地调用由系统服务返回的SensorManager
实例来实现这一点。
总结
这些秘籍展示了如何使用 Android 来使用地图、用户位置和设备传感器数据,将用户周围的信息集成到您的应用中。我们还讨论了如何利用设备的摄像头和麦克风,允许用户捕捉,有时解释他们周围的事情。最后,通过使用媒体 API,您学习了如何获取媒体内容,无论是用户在本地捕获的还是从 Web 上远程下载的,并在您的应用中回放这些内容。在下一章,我们将讨论如何使用 Android 的许多持久性技术来存储设备上的非易失性数据。
五、持久化数据
即使在将尽可能多的用户数据转移到云中的宏伟架构中,移动应用的短暂性也总是要求至少一些用户数据在设备上本地持久存储。这些数据可能包括来自保证离线访问的 web 服务的缓存响应,也可能包括用户为特定应用行为设置的首选项。Android 提供了一系列有用的框架来消除使用文件和数据库来保存信息的痛苦。
5–1。制作首选项屏幕
问题
您需要创建一种简单的方法来存储、更改和显示应用中的用户首选项和设置。
解决办法
(API 一级)
使用PreferenceActivity
和 XML Preference
层次结构一次性提供用户界面、键/值组合和持久性。使用这种方法将创建一个与 Android 设备上的设置应用一致的用户界面,保持用户的体验与他们的期望一致。
在 XML 中,可以定义一个或多个屏幕的完整集合,显示相关的设置,并使用PreferenceScreen
、PreferenceCategory
和相关的Preference
元素对其进行分类。然后,活动可以使用很少的代码为用户加载这个层次结构。
它是如何工作的
清单 5–1 和 5–2 提供了一个 Android 应用的基本设置示例。XML 定义了两个屏幕,其中包含这个框架支持的所有常见首选项类型。请注意,一个屏幕嵌套在另一个屏幕中;当用户从根屏幕点击相关列表项时,将显示内部屏幕。
清单 5–1。 ??【RES/XML/settings . XML】
<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <EditTextPreference android:key="namePref" android:title="Name" android:summary="Tell Us Your Name" android:defaultValue="Apress" /> <CheckBoxPreference android:key="morePref" android:title="Enable More Settings" android:defaultValue="false" /> <PreferenceScreen android:key="moreScreen" android:title="More Settings" android:dependency="morePref"> <ListPreference android:key="colorPref" android:title="Favorite Color" android:summary="Choose your favorite color" android:entries="@array/color_names" android:entryValues="@array/color_values" android:defaultValue="GRN" /> <PreferenceCategory android:title="Location Settings"> <CheckBoxPreference android:key="gpsPref" android:title="Use GPS Location" android:summary="Use GPS to Find You" android:defaultValue="true" /> <CheckBoxPreference android:key="networkPref" android:title="Use Network Location" android:summary="Use Network to Find You" android:defaultValue="true" /> </PreferenceCategory> </PreferenceScreen> </PreferenceScreen>
**清单 5–2。**RES/values/arrays . XML
<?xml version="1.0" encoding="utf-8"?> <resources> <string-array name="color_names"> <item>Black</item> <item>Red</item> <item>Green</item> </string-array>
<string-array name="color_values"> <item>BLK</item> <item>RED</item> <item>GRN</item> </string-array> </resources>
首先注意用于创建 XML 文件的约定。虽然这个资源可以从任何目录(比如 res/layout)展开,但是惯例是将它们放在项目的一个通用目录中,这个目录的名称简单地为“xml”
另外,请注意,我们为每个Preference
对象提供了一个android:key
属性,而不是android:id
。当每个存储的值通过一个SharedPreferences
对象在应用的其他地方被引用时,将使用键来访问它。此外,PreferenceActivity
还包含了findPreference()
方法,用于获取对 Java 代码中一个膨胀的Preference
的引用,这比使用findViewById();
更高效,而且findPreference()
也将键作为参数。
展开后,根首选项屏幕显示一个列表,其中包含以下三个选项(按顺序排列):
标题为“名称”的项目
EditTextPreference
的实例,存储一个字符串值。
点击此项将显示一个文本框,供用户键入新的首选项值。
标题为“启用更多设置”的项目,旁边有一个复选框
CheckBoxPreference 的实例,它存储一个布尔值。
点击此项将切换复选框的选中状态。
标题为“更多设置”的项目
点击此项将加载另一个包含更多项目的首选项屏幕。
当用户点击“更多设置”项目时,第二个屏幕显示三个以上的项目:一个ListPreference
项目和另外两个由PreferenceCategory
组合在一起的CheckBoxPreference
。PreferenceCategory
是一种在列表中创建分节符和标题的简单方法,用于对实际的首选项进行分组。
ListPreference
是示例中使用的最终首选项类型。这个项目需要两个数组参数(尽管它们可以被设置为同一个数组),这两个参数代表用户可以从中选择的一组选项。android:entries
数组是要显示的可读项目列表,而android:entryValues
数组表示要存储的实际值。
所有偏好项也可以选择性地为它们设置默认值。但是,该值不会自动加载。当显示PreferenceActivity
或调用PreferenceManager.setDefaultValues()
时,它将第一次加载这个 XML 文件。
现在让我们看看PreferenceActivity
将如何加载和管理它。参见清单 5–3。
清单 5–3。 偏好行动中的活动
`public class SettingsActivity extends PreferenceActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Load preference data from XML
addPreferencesFromResource(R.xml.settings);
}
}`
向用户显示首选项并允许他们进行更改所需要的只是调用addPreferencesFromResource()
。不需要用PreferenceActivity
来调用setContentView()
,因为addPreferencesFromResource()
会放大 XML 并显示它。然而,可以提供一个自定义布局,只要它包含一个设置了android:id="@android:id/list"
属性的ListView
,这是PreferenceActivity
将加载首选项的地方。
出于控制访问的唯一目的,也可以将偏好项放在列表中。在本例中,我们将“启用更多设置”项放在列表中,只是为了允许用户启用或禁用对第二个PreferenceScreen
的访问。为了实现这一点,我们的嵌套PreferenceScreen
包含了android:dependency
属性,该属性将其启用状态链接到另一个首选项的状态。每当引用的首选项未设置或为假时,该首选项将被禁用。
当这个活动加载时,您会看到类似于 Figure 5–1 的内容。
图 5–1。 首选屏幕在行动
根PreferenceScreen
(左)首先显示。如果用户点击“更多设置”,将显示第二个屏幕(右侧)。
加载默认值和访问首选项
通常,像这样的PreferenceActivity
不是应用的根。通常,如果设置了默认值,在用户访问设置之前,应用的其余部分可能需要访问这些值(第一种情况下将加载默认值)。因此,在应用中的其他地方调用下面的方法会很有帮助,这样可以确保在使用默认值之前将其加载。
PreferenceManager.setDefaultValues(Context context, int resId, boolean readAgain);
这个方法可能会被调用多次,默认值将不会被再次加载。它可以放在主活动中,以便在第一次启动时调用,或者放在一个公共位置,在访问共享首选项之前调用。
使用这种机制存储的首选项被放入默认的共享首选项对象中,可以使用任何Context
指针访问该对象
PreferenceManager.getDefaultSharedPreferences(Context context);
一个示例活动将加载我们上一个示例中设置的默认值,并访问一些存储的当前值,看起来类似于清单 5–4。
清单 5–4。 活动加载偏好默认值
`public class HomeActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//Load the preference defaults
PreferenceManager.setDefaultValues(this, R.xml.settings, false);
}
@Override
public void onResume() {
super.onResume();
//Access the current settings
SharedPreferences settings =
PreferenceManager.getDefaultSharedPreferences(this);
String name = settings.getString("namePref", "");
boolean isMoreEnabled = settings.getBoolean("morePref", false);
}
}`
调用setDefaultValues()
将在首选项存储中为 XML 文件中包含android:defaultValue
属性的任何项目创建一个值。这将使应用可以访问它们,即使用户尚未访问设置屏幕。
然后可以使用一组类型化的访问器函数在SharedPreferences
对象上访问这些值。如果 preference key 的值尚不存在,这些访问器方法中的每一个都需要返回 preference key 的名称和默认值。
5–2。持久化简单数据
问题
您的应用需要一种简单、低开销的方法来将基本数据(如数字和字符串)存储在持久存储中。
解决办法
(API 一级)
使用SharedPreferences
对象,应用可以快速创建一个或多个持久存储,数据可以保存在这些存储中,供以后检索。在底层,这些对象实际上作为 XML 文件存储在应用的用户数据区。然而,与直接从文件中读写数据不同,SharedPreferences
为持久化基本数据类型提供了一个有效的框架。
创建多个 SharedPreferences(而不是将所有数据都转储到默认对象中)可能是一个好习惯,尤其是当您存储的数据有保质期时。请记住,使用 XML 和PreferenceActivity
框架存储的所有首选项也存储在默认位置——如果您想存储一组与登录用户相关的项目,该怎么办?当该用户注销时,您将需要删除随之而来的所有持久化数据。如果您将所有数据存储在默认首选项中,您很可能需要单独删除每个项目。但是,如果您只是为这些设置创建一个首选项对象,那么注销就像调用SharedPreferences.clear()
一样简单。
它是如何工作的
让我们看一个使用SharedPreferences
持久化简单数据的实际例子。清单 5–5 和 5–6 为用户创建一个数据输入表单,向远程服务器发送一条简单的消息。为了帮助用户,我们将记住他们在每个字段中输入的所有数据,直到发出成功的请求。这将允许用户离开屏幕(或被短信或电话打断),而不必再次输入他们的所有信息。
清单 5–5。 res/layout/form.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent">
<TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Email:" android:padding="5dip" /> <EditText android:id="@+id/email" android:layout_width="fill_parent" android:layout_height="wrap_content" android:singleLine="true" /> <CheckBox android:id="@+id/age" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Are You Over 18?" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Message:" android:padding="5dip" /> <EditText android:id="@+id/message" android:layout_width="fill_parent" android:layout_height="wrap_content" android:minLines="3" android:maxLines="3" /> <Button android:id="@+id/submit" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Submit" /> </LinearLayout>
清单 5–6。 有持久性的录入表单
`public class FormActivity extends Activity implements View.OnClickListener {
EditText email, message;
CheckBox age;
Button submit;
SharedPreferences formStore;
boolean submitSuccess = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.form);
email = (EditText)findViewById(R.id.email);
message = (EditText)findViewById(R.id.message);`
` age = (CheckBox)findViewById(R.id.age);
submit = (Button)findViewById(R.id.submit);
submit.setOnClickListener(this);
//Retrieve or create the preferences object
formStore = getPreferences(Activity.MODE_PRIVATE);
}
@Override
public void onResume() {
super.onResume();
//Restore the form data
email.setText(formStore.getString("email", ""));
message.setText(formStore.getString("message", ""));
age.setChecked(formStore.getBoolean("age", false));
}
@Override
public void onPause() {
super.onPause();
if(submitSuccess) {
//Editor calls can be chained together
formStore.edit().clear().commit();
} else {
//Store the form data
SharedPreferences.Editor editor = formStore.edit();
editor.putString("email", email.getText().toString());
editor.putString("message", message.getText().toString());
editor.putBoolean("age", age.isChecked());
editor.commit();
}
}
@Override
public void onClick(View v) {
//DO SOME WORK SUBMITTING A MESSAGE
//Mark the operation successful
submitSuccess = true;
//Close
finish();
}
}`
我们从一个典型的用户表单开始,两个简单的EditText
输入字段和一个CheckBox
。当创建活动时,我们使用Activity.getPreferences()
收集一个SharedPreferences
对象,这是所有持久化数据将被存储的地方。如果在任何时候活动由于除成功提交之外的原因(由布尔成员控制)而暂停,表单的当前状态将被快速加载到首选项中并持久化。
**注意:**当使用Editor
将数据保存到SharedPreferences
中时,务必记住在更改完成后调用commit()
或apply()
。否则,您的更改将不会被保存。
相反,每当活动变得可见时,onResume()
用存储在 preferences 对象中的最新信息加载用户界面。如果不存在首选项,因为它们已被清除或从未创建(第一次启动),则表单被设置为空白。
当用户按下 Submit 并且假表单成功提交时,随后对onPause()
的调用将清除 preferences 中任何存储的表单数据。因为所有这些操作都是在私有首选项对象上完成的,所以清除数据不会影响可能已使用其他方式存储的任何用户设置。
注意:Editor
调用的 方法总是返回同一个Editor
对象,允许它们在某些地方链接在一起,这样做可以让你的代码更具可读性。
共享共享的首选项
前面的例子说明了在单个活动的上下文中使用单个SharedPreferences
对象,该活动具有从Activity.getPreferences()
获得的对象。说实话,这个方法实际上只是一个方便的Context.getSharedPreferences()
包装器,它将活动名作为首选商店名传递。如果您存储的数据最好在两个或多个 Activity 实例之间共享,那么调用getSharedPreferences()
并传递一个更通用的名称可能更有意义,这样就可以很容易地从代码中的不同位置访问它。参见清单 5–7。
清单 5–7。 使用相同偏好的两个活动
`public class ActivityOne extends Activity {
public static final String PREF_NAME = "myPreferences";
private SharedPreferences mPreferences;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPreferences = getSharedPreferences(PREF_NAME, Activity.MODE_PRIVATE);
}
}
public class ActivityTwo extends Activity {
private SharedPreferences mPreferences;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPreferences = getSharedPreferences(ActivityOne.PREF_NAME,
Activity.MODE_PRIVATE);
}
}`
在这个例子中,两个活动类使用相同的名称(定义为常量字符串)检索SharedPreferences
对象,因此它们将访问相同的偏好数据集。此外,两个引用甚至指向首选项的同一个实例 ,因为框架为每组SharedPreferences
(一组由其名称定义)创建了一个单例对象。这意味着在一方所做的更改会立即反映到另一方。
A Note About Mode
Context.getSharedPreferences()
也需要一个模式参数。传递 0 或MODE_PRIVATE
提供了默认行为,只允许创建首选项的应用(或另一个具有相同用户 ID 的应用)获得读写访问权。此方法支持两个以上的模式参数;MODE_WORLD_READABLE
和MODE_WORLD_WRITEABLE
。这些模式允许其他应用通过对其创建的文件设置适当的用户权限来访问这些首选项。但是,外部应用仍然需要一个有效的上下文,指向创建首选项文件的包。
例如,假设您用包com.examples.myfirstapplication
在应用中创建了SharedPreferences with world readable permission
。为了从第二个应用访问这些首选项,第二个应用将使用以下代码获取它们:
Context otherContext = createPackageContext("com.examples.myfirstapplication", 0); SharedPreferences externalPreferences = otherContext.getSharedPreferences(PREF_NAME, 0);
**注意:**如果您选择使用 mode 参数来允许外部访问,请确保您在调用getSharedPreferences()
时提供的模式是一致的。该模式仅在第一次创建首选项文件时使用,因此在不同时间使用不同的模式参数调用SharedPreferences
只会导致混淆。
5–3 岁。读写文件
问题
您的应用需要从外部文件读入数据,或者写出更复杂的数据以实现持久性。
解决办法
(API 一级)
有时,使用文件系统是无可替代的。Android 支持所有用于创建、读取、更新和删除(CRUD)操作的标准 Java 文件 I/O,以及一些额外的助手,使访问特定位置的文件更加方便。应用可以在三个主要位置处理文件:
内存储器
外部存储器
用于读写文件数据的外部可安装空间。
需要 API 级别 4+中的WRITE_EXTERNAL_STORAGE
权限。
通常,这是设备中的物理 SD 卡。
素材
APK 包中受保护的只读空间。
对不能/不应该编译的本地资源有好处。
虽然处理文件数据的底层机制保持不变,但我们将研究使处理每个目的地略有不同的细节。
它是如何工作的
如前所述,传统的 Java FileInputStream
和FileOutputStream
类构成了访问文件数据的主要方法。事实上,您可以随时使用绝对路径位置创建一个File
实例,并开始传输数据。然而,由于不同设备上的根路径不同,并且某些目录受到应用的保护,我们推荐一些稍微更有效的方法来处理文件。
内部存储
为了创建或修改文件在内存中的位置,请使用Context.openFileInput()
和Context.openFileOutput()
方法。这些方法只需要文件名作为参数,而不是整个路径,并且将引用与应用的受保护目录空间相关的文件,而不考虑特定设备上的确切路径。参见清单 5–8。
清单 5–8。 在内存上 CRUD 一个文件
`public class InternalActivity extends Activity {
private static final String FILENAME = "data.txt";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView tv = new TextView(this);
setContentView(tv);
//Create a new file and write some data`
` try {
FileOutputStream mOutput = openFileOutput(FILENAME, Activity.MODE_PRIVATE);
String data = "THIS DATA WRITTEN TO A FILE";
mOutput.write(data.getBytes());
mOutput.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//Read the created file and display to the screen
try {
FileInputStream mInput = openFileInput(FILENAME);
byte[] data = newbyte[128];
mInput.read(data);
mInput.close();
String display = new String(data);
tv.setText(display.trim());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//Delete the created file
deleteFile(FILENAME);
}
}`
这个例子使用Context.openFileOutput()
将一些简单的字符串数据写到一个文件中。使用此方法时,如果文件不存在,将会创建该文件。它有两个参数,一个文件名和一个操作模式。在这种情况下,我们通过将模式定义为MODE_PRIVATE
来使用默认操作。这种模式会用每个新的写操作覆盖文件;如果你喜欢每次写在现有文件的末尾,使用MODE_APPEND
。
写操作完成后,该示例使用Context.openFileInput()
打开 InputStream 并读取文件数据,它只需要再次将文件名作为参数。数据被读入一个字节数组,并通过 TextView 显示给用户界面。完成操作后,Context.deleteFile()
用于从存储器中删除文件。
**注意:**数据以字节的形式写入文件流,因此更高级别的数据(甚至字符串)必须转换成这种格式或从这种格式转换出来。
这个例子没有留下文件的痕迹,但是我们鼓励你尝试同样的例子,不要在最后运行deleteFile()
来保存文件。将 DDMS 与仿真器或解锁的设备一起使用,您可以查看文件系统,并可以在相应的应用数据文件夹中找到该应用创建的文件。
因为这些方法是Context
的一部分,并不绑定到 Activity,所以这种类型的文件访问可以发生在应用中你需要的任何地方,比如一个BroadcastReceiver
或者甚至一个定制类。许多系统构造要么是Context
的子类,要么在它们的回调中传递对它的引用。这允许在任何地方进行相同的打开/关闭/删除操作。
外部存储
内部存储和外部存储的主要区别在于外部存储是可装载的。这意味着用户可以将他们的设备连接到计算机,并可以选择将外部存储作为可移动磁盘安装在 PC 上。通常,存储本身是物理可移动的(如 SD 卡),但这不是平台的要求。
**重要提示:**写入设备的外部存储将需要您向应用清单添加一个android.permission.WRITE_EXTERNAL_STORAGE
声明。
在外部安装或物理移除设备的外部存储器期间,应用无法访问该存储器。因此,通过检查Environment.getExternalStorageState()
来检查外部存储器是否准备好总是谨慎的。
让我们修改文件示例,对设备的外部存储进行同样的操作。参见清单 5–9。
清单 5–9。 在外部存储器上创建一个文件
`public class ExternalActivity extends Activity {
private static final String FILENAME = "data.txt";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView tv = new TextView(this);
setContentView(tv);
//Create the file reference
File dataFile = new File(Environment.getExternalStorageDirectory(), FILENAME);
//Check if external storage is usable
if(!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
Toast.makeText(this, "Cannot use storage.", Toast.LENGTH_SHORT).show();
finish();
return;
}
//Create a new file and write some data
try {
FileOutputStream mOutput = new FileOutputStream(dataFile, false);
String data = "THIS DATA WRITTEN TO A FILE";
mOutput.write(data.getBytes());
mOutput.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//Read the created file and display to the screen
try {
FileInputStream mInput = new FileInputStream(dataFile);
byte[] data = newbyte[128];
mInput.read(data);
mInput.close();
String display = new String(data);
tv.setText(display.trim());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//Delete the created file
dataFile.delete();
}
}`
对于外部存储,我们利用了更多的传统 Java 文件 I/O。使用外部存储的关键是调用Environment.getExternalStorageDirectory()
来检索设备外部存储位置的根路径。
在进行任何操作之前,首先用Environment.getExternalStorageState()
检查设备外部存储器的状态。如果返回值是除了Environment.MEDIA_MOUNTED
之外的任何值,我们将不会继续,因为存储不能被写入,所以活动被关闭。否则,可以创建新文件并开始操作。
输入和输出流现在必须使用默认的 Java 构造函数,而不是使用Context
方便的方法。输出流的默认行为是覆盖当前文件,如果当前文件不存在,则创建它。如果您的应用每次写入都必须追加到现有文件的末尾,请将FileOutputStream
构造函数中的布尔参数更改为 true。
通常,在外部存储器上为应用文件创建一个特殊的目录是有意义的。我们可以简单地使用更多的 Java 文件 API 来实现这一点。参见清单 5–10。
清单 5–10。 在新目录下 CRUD 一个文件
`public class ExternalActivity extends Activity {
private static final String FILENAME = "data.txt";
private static final String DNAME = "myfiles";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView tv = new TextView(this);
setContentView(tv);
//Create a new directory on external storage
File rootPath = new File(Environment.getExternalStorageDirectory(), DNAME);
if(!rootPath.exists()) {
rootPath.mkdirs();
}
//Create the file reference
File dataFile = new File(rootPath, FILENAME);
//Create a new file and write some data
try {
FileOutputStream mOutput = new FileOutputStream(dataFile, false);
String data = "THIS DATA WRITTEN TO A FILE";
mOutput.write(data.getBytes());
mOutput.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//Read the created file and display to the screen
try {
FileInputStream mInput = new FileInputStream(dataFile);
byte[] data = newbyte[128];
mInput.read(data);
mInput.close();
String display = new String(data);
tv.setText(display.trim());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//Delete the created file
dataFile.delete();
}
}`
在本例中,我们在外部存储目录中创建新的目录路径,并将该新位置用作数据文件的根位置。一旦使用新的目录位置创建了文件引用,示例的其余部分是相同的。
5–4 岁。将文件用作资源
问题
您的应用必须利用 Android 无法编译成资源 id 的格式的资源文件。
解决方案
(API 一级)
使用 Assets 目录存放应用需要读取的文件,例如本地 HTML、CSV 或专有数据。素材目录是 Android 应用中文件的受保护资源位置。放置在此目录中的文件将与最终的 APK 捆绑在一起,但不会被处理或编译。像所有其他应用资源一样,素材中的文件是只读的。
它是如何工作的
我们在本书中已经看到了一些具体的例子,可以使用素材将内容直接加载到小部件中,比如WebView
和MediaPlayer
。然而,在大多数情况下,最好使用传统的InputStream
来访问素材。清单 5–11 和 5–12 提供了一个从素材中读取私有逗号分隔值(CSV)文件并显示在屏幕上的示例。
清单 5–11。 素材/数据. csv
John,38,Red Sally,42,Blue Rudy,31,Yellow
清单 5–12。 从素材文件中读取
`public class AssetActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
TextView tv = new TextView(this);
setContentView(tv);
try {
//Access application assets
AssetManager manager = getAssets();
//Open our data file
InputStream mInput = manager.open("data.csv");
//Read data in
byte[] data = newbyte[128];
mInput.read(data);
mInput.close();
//Parse the CSV data and display
String raw = new String(data);
ArrayList cooked = parse(raw.trim());
StringBuilder builder = new StringBuilder();
for(Person piece : cooked) {
builder.append(String.format("%s is %s years old, and likes the color %s",
piece.name, piece.age, piece.color));
builder.append('\n');
}
tv.setText(builder.toString());`
` } catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/* Simple CSV Parser */
private static finalintCOL_NAME = 0;
private static finalintCOL_AGE = 1;
private static finalintCOL_COLOR = 2;
private ArrayList parse(String raw) {
ArrayList results = new ArrayList();
Person current = null;
StringTokenizer st = new StringTokenizer(raw,",\n");
int state = COL_NAME;
while(st.hasMoreTokens()) {
switch(state) {
case COL_NAME:
current = new Person();
current.name = st.nextToken();
state = COL_AGE;
break;
case COL_AGE:
current.age = st.nextToken();
state = COL_COLOR;
break;
case COL_COLOR:
current.color = st.nextToken();
results.add(current);
state = COL_NAME;
break;
}
}
return results;
}
privateclass Person {
public String name;
public String age;
public String color;
public Person()
}
}`
访问 Assets 中的文件的关键在于使用AssetManager
,这将允许应用打开当前驻留在 Assets 目录中的任何资源。将我们感兴趣的文件名传递给AssetManager.open()
会返回一个 InputStream 供我们读取文件数据。将流读入内存后,该示例将原始数据传递给解析例程,并在用户界面上显示结果。
解析 CSV
这个例子还展示了一个简单的方法,从一个 CSV 文件中获取数据,并将其解析成一个模型对象(在本例中称为Person
)。这里使用的方法将整个文件读入一个字节数组,作为单个字符串进行处理。当要读取的数据量非常大时,这种方法不是最有效的内存方法,但是对于像这样的小文件,这种方法就可以了。
原始字符串被传递到 StringTokenizer 实例中,同时传递的还有用作标记断点的必需字符:逗号和换行符。此时,可以按顺序处理文件的每个单独的块。使用基本的状态机方法,来自每一行的数据被插入到新的Person
实例中,并被加载到结果列表中。
5–5 岁。管理数据库
问题
您的应用需要持久化数据,这些数据可以作为子集或单个记录进行查询或修改。
解决办法
(API 一级)
在一个SQLiteOpenHelper
的帮助下创建一个SQLiteDatabase
来管理您的数据存储。SQLite 是一种快速、轻量级的数据库技术,它利用 SQL 语法来构建查询和管理数据。对 SQLite 的支持内置于 Android SDK 中,这使得在应用中设置和使用 SQLite 变得非常容易。
它是如何工作的
定制SQLiteOpenHelper
允许您管理数据库模式本身的创建和修改。它也是一个很好的地方,可以在创建数据库时将您可能需要的任何初始值或默认值插入到数据库中。清单 5–13 是一个定制助手的例子,它创建一个数据库,用一个表来存储关于人的基本信息。
清单 5–13。 自定义 SQLiteOpenHelper
`public class MyDbHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "mydb";
private static final int DB_VERSION = 1;
public static final String TABLE_NAME = "people";
public static final String COL_NAME = "pName";
public static final String COL_DATE = "pDate";
private static final String STRING_CREATE =`
` "CREATE TABLE "+TABLE_NAME+" (_id INTEGER PRIMARY KEY AUTOINCREMENT, "
+COL_NAME+" TEXT, "+COL_DATE+" DATE);";
public MyDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
//Create the database table
db.execSQL(STRING_CREATE);
//You may also load initial values into the database here
ContentValues cv = new ContentValues(2);
cv.put(COL_NAME, "John Doe");
//Create a formatter for SQL date format
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
cv.put(COL_DATE, dateFormat.format(new Date())); //Insert 'now' as the date
db.insert(TABLE_NAME, null, cv);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
//For now, clear the database and re-create
db.execSQL("DROP TABLE IF EXISTS "+TABLE_NAME);
onCreate(db);
}
}`
数据库需要的关键信息是名称和版本号。创建和升级 SQLiteDatabase 确实需要一点 SQL 知识,所以如果您不熟悉一些语法,我们建议您浏览一下 SQL 参考资料。助手将在任何时候访问这个特定的数据库时调用onCreate()
,使用SQLiteOpenHelper.getReadableDatabase()
或SQLiteOpenHelper.getWritableDatabase()
,如果它还不存在的话。
该示例将表名和列名抽象为常量以供外部使用(这是一个很好的习惯)。下面是实际的 SQL 创建字符串,它在onCreate()
中用来创建我们的表:
CREATE TABLE people (_id INTEGER PRIMARY KEY AUTOINCREMENT, pName TEXT, pAge INTEGER, pDate DATE);
在 Android 中使用 SQLite 时,数据库必须进行少量的格式化,以便与框架一起正常工作。它的大部分是为您创建的,但是您创建的表必须有一部分是用于_id
的列。该字符串的其余部分为表中的每条记录再创建两列:
使用ContentValues
对象将数据插入数据库。该示例说明了如何在创建数据库时使用ContentValues
向数据库中插入一些默认数据。SQLiteDatabase.insert()
采用表名、空列 hack 和表示要插入的记录的ContentValues
作为参数。
这里没有使用空列黑客,但是它有一个可能对您的应用至关重要的目的。SQL 不能将一个完全为空的值插入到数据库中,尝试这样做将会导致错误。如果您的实现有可能将一个空的ContentValues
传递给insert()
,则使用空列 hack 来插入一个记录,其中引用列的值为空。
关于升级的说明
SQLiteOpenHelper
也很好地帮助您在应用的未来版本中迁移数据库模式。每当数据库被访问,但是磁盘上的版本与当前版本不匹配(意味着构造函数中传递的版本),就会调用onUpgrade()
。
在我们的例子中,我们采用了懒人的方式,简单地删除现有的数据库并重新创建它。实际上,如果数据库包含用户输入的数据,这可能不是合适的方法;他们可能不会太高兴看到它消失。所以让我们暂时离题,看一个可能更有用的onUpgrade()
的例子。例如,在应用的整个生命周期中使用以下三个数据库:
版本 1:应用的首次发布
版本 2:应用升级,包括电话号码字段
版本 3:应用升级,包括插入的日期条目
我们可以利用onUpgrade()
来改变现有的数据库,而不是删除当前所有的信息。参见清单 5–14。
**清单 5–14。***on upgrade()*的样本
@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { //Upgrade from v1\. Adding phone number if(oldVersion<= 1) { db.execSQL("ALTER TABLE "+TABLE_NAME+" ADD COLUMN phone_number INTEGER;"); } //Upgrade from v2\. Add entry date if(oldVersion <= 2) { db.execSQL("ALTER TABLE "+TABLE_NAME+" ADD COLUMN entry_date DATE;"); } }
在这个例子中,如果用户的现有数据库版本是 1,那么这两个语句都将被调用来向数据库添加列。如果他们已经有了版本 2,那么只需要调用后面的语句来添加条目日期列。在这两种情况下,应用数据库中的任何现有数据都将被保留。
使用数据库
回到我们最初的示例,让我们看看一个活动如何利用我们创建的数据库。参见清单 5–15 和清单 5–16。
清单 5–15。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <EditText android:id="@+id/name" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <Button android:id="@+id/add" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Add New Person" /> <ListView android:id="@+id/list" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout>
清单 5–16。 查看和管理活动数据库
`public class DbActivity extends Activity implements View.OnClickListener,
AdapterView.OnItemClickListener {
EditText mText;
Button mAdd;
ListView mList;
MyDbHelper mHelper;
SQLiteDatabase mDb;
Cursor mCursor;
SimpleCursorAdapter mAdapter;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mText = (EditText)findViewById(R.id.name);
mAdd = (Button)findViewById(R.id.add);
mAdd.setOnClickListener(this);
mList = (ListView)findViewById(R.id.list);
mList.setOnItemClickListener(this);
mHelper = new MyDbHelper(this);
}
@Override
public void onResume() {
super.onResume();
//Open connections to the database
mDb = mHelper.getWritableDatabase();
String[] columns = new String[] {"_id", MyDbHelper.COL_NAME, MyDbHelper.COL_DATE};
mCursor = mDb.query(MyDbHelper.TABLE_NAME, columns, null, null, null, null, null);
//Refresh the list
String[] headers = new String[] {MyDbHelper.COL_NAME, MyDbHelper.COL_DATE};
mAdapter = new SimpleCursorAdapter(this, android.R.layout.two_line_list_item,
mCursor, headers, newint[]{android.R.id.text1, android.R.id.text2});
mList.setAdapter(mAdapter);
}
@Override
public void onPause() {
super.onPause();
//Close all connections
mDb.close();
mCursor.close();
}
@Override
public void onClick(View v) {
//Add a new value to the database
ContentValues cv = new ContentValues(2);
cv.put(MyDbHelper.COL_NAME, mText.getText().toString());
//Create a formatter for SQL date format
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
cv.put(MyDbHelper.COL_DATE, dateFormat.format(new Date())); //Insert 'now' as the date
mDb.insert(MyDbHelper.TABLE_NAME, null, cv);
//Refresh the list
mCursor.requery();
mAdapter.notifyDataSetChanged();
//Clear the edit field
mText.setText(null);
}
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
//Delete the item from the database
mCursor.moveToPosition(position);
//Get the id value of this row
String rowId = mCursor.getString(0); //Column 0 of the cursor is the id
mDb.delete(MyDbHelper.TABLE_NAME, "_id = ?", new String[]);
//Refresh the list
mCursor.requery();
mAdapter.notifyDataSetChanged();
}
}`
在这个例子中,我们利用我们的客户SQLiteOpenHelper
来访问数据库实例,并将数据库中的每条记录作为列表显示给用户界面。来自数据库的信息如果以Cursor
的形式返回,这是一个设计用来读取、写入和遍历查询结果的接口。
当活动变得可见时,进行数据库查询以返回“people”表中的所有记录。必须将列名数组传递给查询,以告诉数据库要返回哪些值。query()
的其余参数旨在缩小选择数据集,我们将在下一个秘籍中对此进行进一步研究。当不再需要数据库和游标连接时,关闭它们是很重要的。在本例中,我们在onPause()
中这样做,此时活动不再处于前台。
SimpleCursorAdapter
用于将数据库中的数据映射到标准的 Android 两行列表项目视图中。string 和 int 数组参数构成映射;string 数组中每一项的数据都将插入到视图中,并在 int 数组中显示相应的 id 值。注意,这里传递的列名列表与传递给查询的数组略有不同。这是因为我们将需要知道其他操作的记录 id,但是在将数据映射到用户界面时这是不必要的。
用户可以在文本字段中输入姓名,然后按“添加新人”按钮来创建新的内容值并将其插入到数据库中。此时,为了让 UI 显示更改,我们调用了Cursor.requery()
和ListAdapter.notifyDataSetChanged()
。
相反,点击列表中的项目将从数据库中删除该指定项目。为了实现这一点,我们必须构造一个简单的 SQL 语句,告诉数据库只删除 _id 值与该选择匹配的记录。此时,光标和列表适配器再次被刷新。
通过将光标移动到所选位置并调用getString(0)
来获得列索引零的值,从而获得选择的 _id 值。此请求返回 _id,因为在列列表中传递给查询的第一个参数(索引 0)是“_id”delete 语句由两个参数组成:语句字符串和参数。对于字符串中出现的每个问号,将在语句中插入传递的数组中的一个参数。
5 至 6 岁。查询数据库
问题
您的应用使用 SQLiteDatabase,您需要返回其中包含的数据的特定子集。
解决办法
(API 一级)
使用完全结构化的 SQL 查询,为特定数据创建过滤器并从数据库返回这些子集非常简单。有几种重载形式的SQLiteDatabase.query()
可以从数据库中收集信息。我们将在这里检查其中最冗长的。
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)
前两个参数简单地定义了查询数据的表,以及我们想要访问的每条记录的列。剩下的参数定义了我们将如何缩小结果的范围。
选择
选择 Args
群组依据
拥有
排序依据
限制
如您所见,所有这些参数都旨在为数据库查询提供 SQL 的全部功能。
它是如何工作的
让我们来看一些示例查询,可以构造这些查询来完成一些常见的实用查询。
String[] COLUMNS = new String[] {COL_NAME, COL_DATE}; String selection = COL_NAME+" = ?"; String[] args = new String[] {"NAME_TO_MATCH"}; Cursor result = db.query(TABLE_NAME, COLUMNS, selection, args, null, null, null, null);
这个查询相当简单。selection 语句只是告诉数据库将 name 列中的任何数据与所提供的参数相匹配(该参数是在“?”位置插入的)在选择字符串中)。
String orderBy = "_id DESC"; String limit = "10"; Cursor result = db.query(TABLE_NAME, COLUMNS, null, null, null, null, orderBy, limit);
这个查询没有特殊的选择标准,而是告诉数据库按照自动递增的 id 值对结果进行排序,最新的(最高的 id)记录排在第一位。limit 子句将返回结果的最大数量设置为 10。
返回日期字段在指定范围内的行(在本例中为 2000 年)。
String[] COLUMNS = new String[] {COL_NAME, COL_DATE}; String selection = "datetime("+COL_DATE+") > datetime(?)"+ " AND datetime("+COL_DATE+") < datetime(?)"; String[] args = new String[] {"2000-1-1 00:00:00","2000-12-31 23:59:59"}; Cursor result = db.query(TABLE_NAME, COLUMNS, selection, args, null, null, null, null);
SQLite 没有为日期保留特定的数据类型,尽管它们允许在创建表时使用 DATE 作为声明类型。但是,标准的 SQL 日期和时间函数可用于创建文本、整数或实数形式的数据表示。这里,我们比较数据库中的值和该范围的开始和结束日期的格式化字符串的返回值。
返回整数字段在指定范围内(在本例中为 7 到 10)的行。
String[] COLUMNS = new String[] {COL_NAME, COL_AGE}; String selection = COL_AGE+"> ? AND "+COL_AGE+"< ?"; String[] args = new String[] {"7","10"}; Cursor result = db.query(TABLE_NAME, COLUMNS, selection, args, null, null, null, null);
这与上一个示例类似,但没有那么冗长。这里,我们只需创建选择语句来返回大于下限但小于上限的值。这两个限制都是作为要插入的参数提供的,因此它们可以在应用中动态设置。
5 至 7 岁。备份数据
问题
您的应用将数据保存在设备上,当用户更换设备或被迫重新安装应用时,您需要为用户提供一种备份和恢复这些数据的方法。
解决办法
(API 一级)
使用设备的外部存储作为安全位置来复制数据库和其他文件。外部存储通常是物理可移动的,允许用户将其放在另一个设备中并进行恢复。即使在不可能的情况下,当用户将其设备连接到计算机时,也可以安装外部存储器,从而进行数据传输。
工作原理
清单 5–17 展示了AsyncTask
的一个实现,它在设备的外部存储器和它在应用数据目录中的位置之间来回复制数据库文件。它还为要实现的活动定义了一个接口,以便在操作完成时得到通知。
清单 5–17。 异步请求备份和恢复
`public class BackupTask extends AsyncTask<String,Void,Integer> {
public interface CompletionListener {
void onBackupComplete();
void onRestoreComplete();
void onError(int errorCode);
}
public static final int BACKUP_SUCCESS = 1;
public static final int RESTORE_SUCCESS = 2;
public static final int BACKUP_ERROR = 3;
public static final int RESTORE_NOFILEERROR = 4;
public static final String COMMAND_BACKUP = "backupDatabase";
public static final String COMMAND_RESTORE = "restoreDatabase";
private Context mContext;
private CompletionListener listener;
public BackupTask(Context context) {
super();
mContext = context;
}
public void setCompletionListener(CompletionListener aListener)
@Override
protected Integer doInBackground(String... params) {
//Get a reference to the database
File dbFile = mContext.getDatabasePath("mydb");
//Get a reference to the directory location for the backup
File exportDir = new File(Environment.getExternalStorageDirectory(), "myAppBackups");
if (!exportDir.exists()) {
exportDir.mkdirs();
}
File backup = new File(exportDir, dbFile.getName());
//Check the required operation
String command = params[0];
if(command.equals(COMMAND_BACKUP)) {
//Attempt file copy
try {
backup.createNewFile();
fileCopy(dbFile, backup);
returnBACKUP_SUCCESS;
} catch (IOException e) {
returnBACKUP_ERROR;
}
} elseif(command.equals(COMMAND_RESTORE)) {
//Attempt file copy
try {
if(!backup.exists()) {
returnRESTORE_NOFILEERROR;
}
dbFile.createNewFile();
fileCopy(backup, dbFile);
return RESTORE_SUCCESS;
} catch (IOException e) {
return BACKUP_ERROR;
}
} else {
return BACKUP_ERROR;
}
}
@Override
protected void onPostExecute(Integer result) {
switch(result) {
case BACKUP_SUCCESS:
if(listener != null) {
listener.onBackupComplete();
}
break;
case RESTORE_SUCCESS:
if(listener != null) {
listener.onRestoreComplete();
}
break;
case RESTORE_NOFILEERROR:
if(listener != null) {
listener.onError(RESTORE_NOFILEERROR);
}
break;
default:
if(listener != null) {
listener.onError(BACKUP_ERROR);
}
}
}
private void fileCopy(File source, File dest) throws IOException {
FileChannel inChannel = new FileInputStream(source).getChannel();
FileChannel outChannel = new FileOutputStream(dest).getChannel();
try {
inChannel.transferTo(0, inChannel.size(), outChannel);
} finally {
if (inChannel != null)
inChannel.close();
if (outChannel != null)
outChannel.close();
}
}
}`
如您所见,当COMMAND_BACKUP
被传递给execute()
时,BackupTask 将命名数据库的当前版本复制到外部存储中的特定目录,当COMMAND_RESTORE
被传递时,将文件复制回来。
一旦执行,任务使用Context.getDatabasePath()
来检索我们需要备份的数据库文件的引用。这一行可以很容易地替换为对Context.getFilesDir()
的调用,访问系统内部存储器上的文件进行备份。还获得了对我们在外部存储器上创建的备份目录的引用。
使用传统的 Java 文件 I/O 复制文件,如果一切成功,注册的监听器会得到通知。在此过程中,任何抛出的异常都会被捕获,并向侦听器返回一个错误。现在让我们来看一个利用该任务备份数据库的活动——参见清单 5–18。
清单 5–18。 活动使用备份任务
`public class BackupActivity extends Activity implements BackupTask.CompletionListener {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//Dummy example database
SQLiteDatabase db = openOrCreateDatabase("mydb", Activity.MODE_PRIVATE, null);
db.close();
}
@Override
public void onResume() {
super.onResume();
if( Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ) {
BackupTask task = new BackupTask(this);
task.setCompletionListener(this);
task.execute(BackupTask.COMMAND_RESTORE);
}
}
@Override
public void onPause() {
super.onPause();
if( Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED) ) {
BackupTask task = new BackupTask(this);
task.execute(BackupTask.COMMAND_BACKUP);
}
}
@Override
public void onBackupComplete() {
Toast.makeText(this, "Backup Successful", Toast.LENGTH_SHORT).show();
}`
` @Override
public void onError(int errorCode) {
if(errorCode == BackupTask.RESTORE_NOFILEERROR) {
Toast.makeText(this, "No Backup Found to Restore",
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Error During Operation: "+errorCode,
Toast.LENGTH_SHORT).show();
}
}
@Override
public void onRestoreComplete() {
Toast.makeText(this, "Restore Successful", Toast.LENGTH_SHORT).show();
}
}`
该活动实现了由BackupTask,
定义的CompletionListener
,因此当操作完成或发生错误时,它会得到通知。出于示例的目的,在应用的数据库目录中创建了一个虚拟数据库。我们调用openOrCreateDatabase()
只是为了允许创建一个文件,所以连接在创建之后会立即关闭。在正常情况下,这个数据库已经存在,这些行是不必要的。
该示例在每次活动恢复时执行一个恢复操作,向任务注册自己,以便它可以得到通知,并向用户提示状态结果。请注意,检查外部存储是否可用的任务也落到了活动上,如果外部存储不可访问,则不会执行任何任务。当活动暂停时,执行备份操作,这一次不注册回调。这是因为用户对该活动不再感兴趣,所以我们将不需要举杯指出操作结果。
额外积分
这个后台任务可以扩展到将数据保存到基于云的服务中,以获得最大的安全性和数据可移植性。有许多选项可以实现这一点,包括 Google 自己的 web APIs,我们建议您尝试一下。
从 API Level 8 开始,Android 还包括一个用于将数据备份到云服务的 API。这个 API 可能适合您的目的,但是我们不会在这里讨论它。Android 框架不能保证该服务在所有 Android 设备上都可用,并且在撰写本文时还没有 API 来确定用户拥有的设备是否支持 Android 备份,因此不建议对关键数据使用该服务。
5–8。共享您的数据库
问题
您的应用希望将其维护的数据库内容提供给设备上的其他应用。
解决办法
(API 一级)
创建一个ContentProvider
作为应用数据的外部接口。ContentProvider
通过类似数据库的接口query()
、insert()
、update()
和delete()
向外部请求公开任意一组数据;尽管实现者可以自由设计接口如何映射到实际的数据模型。创建一个 ContentProvider 来公开来自SQLiteDatabase
的数据简单明了。除了一些小的例外,开发人员只需要将调用从提供者传递到数据库。
关于操作哪个数据集的参数通常编码在传递给ContentProvider
的 Uri 中。例如,发送查询 Uri,如
content://com.examples.myprovider/friends
会告诉提供者返回其数据集中“friends”表的信息,而
content://com.examples.myprovider/friends/15
将只指示记录 id 15 从查询中返回。应该注意的是,这些只是系统其余部分使用的约定,您有责任让您创建的ContentProvider
以这种方式运行。ContentProvider
本身并没有为您提供这种功能。
它是如何工作的
首先,要创建一个与数据库交互的ContentProvider
,我们必须有一个可以与之交互的数据库。清单 5–19 是一个示例SQLiteOpenHelper
实现,我们将使用它来创建和访问数据库本身。
清单 5–19。 示例 SQLiteOpenHelper
`public class ShareDbHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "frienddb";
private static final int DB_VERSION = 1;
public static final String TABLE_NAME = "friends";
public static final String COL_FIRST = "firstName";
public static final String COL_LAST = "lastName";
public static final String COL_PHONE = "phoneNumber";`
` private static final String STRING_CREATE =
"CREATE TABLE "+TABLE_NAME+" (_id INTEGER PRIMARY KEY AUTOINCREMENT, "
+COL_FIRST+" TEXT, "+COL_LAST+" TEXT, "+COL_PHONE+" TEXT);";
public ShareDbHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
//Create the database table
db.execSQL(STRING_CREATE);
//Inserting example values into database
ContentValues cv = new ContentValues(3);
cv.put(COL_FIRST, "John");
cv.put(COL_LAST, "Doe");
cv.put(COL_PHONE, "8885551234");
db.insert(TABLE_NAME, null, cv);
cv = new ContentValues(3);
cv.put(COL_FIRST, "Jane");
cv.put(COL_LAST, "Doe");
cv.put(COL_PHONE, "8885552345");
db.insert(TABLE_NAME, null, cv);
cv = new ContentValues(3);
cv.put(COL_FIRST, "Jill");
cv.put(COL_LAST, "Doe");
cv.put(COL_PHONE, "8885553456");
db.insert(TABLE_NAME, null, cv);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
//For now, clear the database and re-create
db.execSQL("DROP TABLE IF EXISTS "+TABLE_NAME);
onCreate(db);
}
}`
总的来说,这个助手相当简单,创建一个表来保存我们的朋友列表,其中只有三列用于存放文本数据。出于此示例的目的,插入了三个行值。现在让我们来看看将这个数据库暴露给其他应用的ContentProvider
——参见清单 5–20 和 5–21。
清单 5–20。 内容提供者的清单声明
<manifest xmlns:android="http://schemas.android.com/apk/res/android" …> <application …> <provider android:name=".FriendProvider" android:authorities="com.examples.sharedb.friendprovider"> </provider> </application> </manifest>
清单 5–20。 为一个数据库提供内容
`public class FriendProvider extends ContentProvider {
public static final Uri CONTENT_URI =
Uri.parse("content://com.examples.sharedb.friendprovider/friends");
public static finalclass Columns {
public static final String _ID = "_id";
public static final String FIRST = "firstName";
public static final String LAST = "lastName";
public static final String PHONE = "phoneNumber";
}
/* Uri Matching */
private static final int FRIEND = 1;
private static final int FRIEND_ID = 2;
private static final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
matcher.addURI(CONTENT_URI.getAuthority(), "friends", FRIEND);
matcher.addURI(CONTENT_URI.getAuthority(), "friends/#", FRIEND_ID);
}
SQLiteDatabase db;
@Override
publicint delete(Uri uri, String selection, String[] selectionArgs) {
int result = matcher.match(uri);
switch(result) {
case FRIEND:
return db.delete(ShareDbHelper.TABLE_NAME, selection, selectionArgs);
case FRIEND_ID:
return db.delete(ShareDbHelper.TABLE_NAME, "_ID = ?",
new String[]{uri.getLastPathSegment()});
default:
return 0;
}
}
@Override
public String getType(Uri uri) {
returnnull;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
long id = db.insert(ShareDbHelper.TABLE_NAME, null, values);
if(id >= 0) {
return Uri.withAppendedPath(uri, String.valueOf(id));
} else {
returnnull;
}
}
@Override
publicboolean onCreate() {
ShareDbHelper helper = new ShareDbHelper(getContext());
db = helper.getWritableDatabase();
returntrue;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder) {
int result = matcher.match(uri);
switch(result) {
case FRIEND:
return db.query(ShareDbHelper.TABLE_NAME, projection, selection,
selectionArgs,null, null, sortOrder);
case FRIEND_ID:
return db.query(ShareDbHelper.TABLE_NAME, projection, "_ID = ?",
new String[]{uri.getLastPathSegment()}, null, null, sortOrder);
default:
returnnull;
}
}
@Override
publicint update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
int result = matcher.match(uri);
switch(result) {
case FRIEND:
return db.update(ShareDbHelper.TABLE_NAME, values, selection,
selectionArgs);
case FRIEND_ID:
return db.update(ShareDbHelper.TABLE_NAME, values, "_ID = ?",
new String[]{uri.getLastPathSegment()});
default:
return 0;
}
}
}`
ContentProvider 必须在应用的清单中用它所表示的授权字符串来声明。这允许从外部应用访问该提供程序,但即使您只在应用内部使用该提供程序,这也是必需的。权限是 Android 用来将Uri
请求匹配到提供者的,因此它应该匹配公共CONTENT_URI
的权限部分。
扩展ContentProvider
时需要覆盖的六种方法是query()
、insert()
、update()
、delete()
、getType()
和onCreate()
。这些方法中的前四个在SQLiteDatabase
中有直接对应的方法,所以只需用适当的参数调用数据库方法。这两者之间的主要区别是ContentProvider
方法传入了一个Uri
,提供者应该检查这个方法以确定操作数据库的哪一部分。
当一个活动或其他系统组件调用其内部ContentResolver
上的相应方法时(您可以在清单 5–21 中看到这一点),或者在活动的情况下,当调用managedQuery()
时,这四个主要的 CRUD 方法在提供者上被调用。
为了遵守本菜谱第一部分提到的Uri
约定,insert()
返回一个Uri
对象,它是通过将新创建的记录 id 附加到路径的末尾而创建的。这个Uri
应该被它的请求者认为是对刚刚创建的记录的直接引用。
其余的方法(query()
、update()
和delete()
)遵循惯例,通过检查传入的Uri
来查看它是引用特定的记录,还是引用整个表。这个任务是在UriMatcher
便利类的帮助下完成的。UriMatcher.match()
方法将Uri
与一组提供的模式进行比较,并将匹配的模式作为 int 返回,如果没有找到匹配的模式,则返回UriMatcher.NO_MATCH
。如果一个Uri
被附加了一个记录 id,那么对数据库的调用将被修改为只影响那个特定的行。
应该通过用UriMatcher.addURI()
提供一组模式来初始化一个UriMatcher
;谷歌建议这一切都在ContentProvider
的静态环境中完成。添加的每个模式还被赋予一个常量标识符,当进行匹配时,该标识符将作为返回值。在提供的模式中可以使用两个通配符:井号(#)将匹配任何数字,星号(*)将匹配任何文本。
我们的例子创建了两个匹配的模式。初始模式与提供的CONTENT_URI
直接匹配,并被用来引用整个数据库表。第二种模式寻找路径的附加数字,该数字将被用来引用该 id 处的记录。
通过onCreate()
中的ShareDbHelper
给出的引用获得对数据库的访问。在决定这种方法是否适用于您的应用时,应该考虑所用数据库的大小。我们的数据库在创建时非常小,但是更大的数据库可能需要很长时间来创建,在这种情况下,在这个操作发生时,主线程不应该被占用;getWritableDatabase()
在这些情况下,可能需要包装在 AsyncTask 中并在后台完成。现在让我们来看一个访问数据的示例活动——参见清单 5–23 和 5–24。
清单 5–23。 AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.examples.sharedb" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="1" /> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".ShareActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <provider android:name=".FriendProvider" android:authorities="com.examples.sharedb.friendprovider"> </provider> </application> </manifest>
清单 5–24。 活动访问内容提供者
`public class ShareActivity extends ListActivity implements AdapterView.OnItemClickListener {
Cursor mCursor;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//List of column names to return from the query for each record
String[] projection = new String[]{FriendProvider.Columns._ID, FriendProvider.Columns.FIRST};
mCursor = managedQuery(FriendProvider.CONTENT_URI, projection, null, null, null);
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_1,
mCursor,
new String[],
newint[]);
ListView list = getListView();
list.setOnItemClickListener(this);
list.setAdapter(adapter);
}
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
mCursor.moveToPosition(position);
Uri uri = Uri.withAppendedPath(FriendProvider.CONTENT_URI, mCursor.getString(0));
String[] projection = new String[]{FriendProvider.Columns.FIRST,
FriendProvider.Columns.LAST,
FriendProvider.Columns.PHONE};
//Get the full record
Cursor cursor = getContentResolver().query(uri, projection, null, null, null);
cursor.moveToFirst();
String message = String.format("%s %s, %s", cursor.getString(0),
cursor.getString(1),cursor.getString(2));
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
}
}`
该示例查询FriendsProvider
中的所有记录,并将它们放入一个列表中,只显示名字列。为了让Cursor
正确地适应列表,我们的投影必须包括 ID 列,即使它没有显示。
如果用户点击列表中的任何一项,就会使用在末尾附加了记录 ID 的 Uri 对提供者进行另一次查询,迫使提供者只返回那一条记录。此外,还提供了一个扩展投影来获取关于这个朋友的所有列数据。
返回的数据被放入一个Toast
中,并被提交给用户查看。光标中的各个字段通过它们的列索引 进行访问,对应于传递给查询的投影中的索引。Cursor.getColumnIndex()
方法也可以用来查询游标,查找与给定列名相关联的索引。
当不再需要某个Cursor
时,它应该总是被关闭,就像我们在用户点击时创建的Cursor
一样。成员mCursor
从未被显式关闭,因为它是由活动管理的。每当使用managedQuery()
创建一个Cursor
时,该活动将打开、关闭和刷新数据以及它自己的正常生命周期。
Figure 5–2 显示了运行此示例以显示提供者内容的结果。
图 5–2。 来自内容提供商的信息
5–9 岁。共享您的其他数据
问题
您希望您的应用将它维护的文件或其他数据提供给设备上的其他应用。
解决方案
(API 三级)
创建一个ContentProvider
作为应用数据的外部接口。ContentProvider
通过类似数据库的接口query()
、insert()
、update()
和delete()
向外部请求公开任意一组数据,尽管实现可以自由设计数据如何从这些方法传递到实际模型。
ContentProvider
可用于向外部请求公开任何类型的应用数据,包括应用的资源和素材。
它是如何工作的
让我们来看一个ContentProvider
实现,它公开了两个数据源:位于内存中的字符串数组,以及存储在应用的 assets 目录中的一系列图像文件。和以前一样,我们必须在清单中使用一个<provider>
标签向 Android 系统声明我们的提供者。参见清单 5–25 和清单 5–26。
清单 5–25。 内容提供者的清单声明
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" …> <application …> <provider android:name=".ImageProvider" android:authorities="com.examples.share.imageprovider"> </provider> </application> </manifest>
清单 5–26。 自定义内容提供者公开素材
`public class ImageProvider extends ContentProvider {
public static final Uri CONTENT_URI =
Uri.parse("content://com.examples.share.imageprovider");
public static final String COLUMN_NAME = "nameString";
public static final String COLUMN_IMAGE = "imageUri";
private String[] mNames;
@Override
publicint delete(Uri uri, String selection, String[] selectionArgs) {
thrownew UnsupportedOperationException("This ContentProvider is read-only");
}
@Override
public String getType(Uri uri) {
returnnull;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
thrownew UnsupportedOperationException("This ContentProvider is read-only");
}
@Override
publicboolean onCreate() {
mNames = new String[] {"John Doe", "Jane Doe", "Jill Doe"};
returntrue;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,String sortOrder) {
MatrixCursor cursor = new MatrixCursor(projection);
for(int i = 0; i < mNames.length; i++) {
//Insert only the columns they requested
MatrixCursor.RowBuilder builder = cursor.newRow();
for(String column : projection) {
if(column.equals("_id")) {
//Use the array index as a unique id
builder.add(i);
}
if(column.equals(COLUMN_NAME)) {
builder.add(mNames[i]);
}
if(column.equals(COLUMN_IMAGE)) {
builder.add(Uri.withAppendedPath(CONTENT_URI, String.valueOf(i)));
}
}
}
return cursor;
}
@Override
publicint update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
thrownew UnsupportedOperationException("This ContentProvider is read-only");
}
@Override
public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
int requested = Integer.parseInt(uri.getLastPathSegment());
AssetFileDescriptor afd;
AssetManager manager = getContext().getAssets();
//Return the appropriate asset for the requested item
try {
switch(requested) {
case 0:
afd = manager.openFd("logo1.png");
break;
case 1:
afd = manager.openFd("logo2.png");
break;
case 2:
afd = manager.openFd("logo3.png");
break;
default:
afd = manager.openFd("logo1.png");
}
return afd;
} catch (IOException e) {
e.printStackTrace();
returnnull;
}
}
}`
正如您可能已经猜到的,该示例公开了三个徽标图像素材。我们为此示例选择的图像如图 5–3 所示。
图 5–3。 示例 logo1.png(左)、logo2.png(中)和 logo3.png(右)存储在素材中
首先注意,因为我们在素材目录中公开只读内容,所以不需要支持继承的方法insert()
、update()
或delete()
,所以我们让这些方法简单地抛出一个UnsupportedOperationException
。
创建提供者时,保存人名的字符串数组被创建,onCreate()
返回 true 这向系统发出信号,表明提供程序已成功创建。提供者为它的Uri
和所有可读的列名公开常量。外部应用将使用这些值来请求数据。
此提供程序仅支持对其中所有数据的查询。为了支持对特定记录或所有内容的子集的条件查询,应用可以处理传入到query()
中的selection
和selectionArgs
的值。在这个例子中,对query()
的任何调用都将构建一个包含所有三个元素的游标。
该提供程序中使用的游标实现是一个MatrixCursor
,它是一个设计用于围绕不在数据库中保存的数据构建的游标。该示例遍历所请求的列列表(投影),并根据所包含的这些列构建每一行。每一行都是通过调用MatrixCursor.newRow()
创建的,它还返回一个用于添加列数据的Builder
实例。应该始终注意将列数据的顺序添加到所请求的投影顺序中。它们应该总是匹配的。
name 列中的值是本地数组中相应的字符串,而 _id 值(Android 需要它来利用返回的带有大多数ListAdapter
的光标)只是作为数组索引返回。每行的 image 列中显示的数据实际上是代表每行图像文件的内容Uri
,它是以提供者的内容Uri
为基础创建的,并附加了数组索引。
当一个外部应用实际上通过ContentResolver.openInputStream()
去检索这个内容时,将调用openAssetFile()
,这个调用已经被覆盖以返回一个指向素材目录中的一个图像文件的AssetFileDescriptor
。这个实现通过再次解构内容Uri
并从末尾检索附加的索引值来确定返回哪个图像文件。
用法举例
让我们看看在 Android 应用的上下文中应该如何实现和访问这个提供者。参见清单 5–27。
清单 5–27。 AndroidManifest.xml
`
`
要实现这个提供者,拥有内容的应用的清单必须声明一个<provider>
标记,指出发出请求时要匹配的ContentProvider
名称和授权。权限值应该与暴露内容的基本部分相匹配Uri
。必须在清单中声明提供程序,以便系统可以实例化并运行它,即使拥有它的应用没有运行。参见清单 5–28 和清单 5–29。
清单 5–28。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:id="@+id/name" android:layout_width="wrap_content" android:layout_height="20dip" android:layout_gravity="center_horizontal" /> <ImageView android:id="@+id/image" android:layout_width="wrap_content" android:layout_height="50dip"
android:layout_gravity="center_horizontal" /> <ListView android:id="@+id/list" android:layout_width="fill_parent" android:layout_height="fill_parent" /> </LinearLayout>
清单 5–29。 从 ImageProvider 读取活动
`public class ShareActivity extends Activity implements AdapterView.OnItemClickListener {
Cursor mCursor;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
String[] projection = new String[]{"_id", ImageProvider.COLUMN_NAME,
ImageProvider.COLUMN_IMAGE};
mCursor = managedQuery(ImageProvider.CONTENT_URI, projection, null, null, null);
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1,mCursor, new String[],
new int[]);
ListView list = (ListView)findViewById(R.id.list);
list.setOnItemClickListener(this);
list.setAdapter(adapter);
}
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
//Seek the cursor to the selection
mCursor.moveToPosition(position);
//Load the name column into the TextView
TextView tv = (TextView)findViewById(R.id.name);
tv.setText(mCursor.getString(1));
ImageView iv = (ImageView)findViewById(R.id.image);
try {
//Load the content from the image column into the ImageView
InputStream in =
getContentResolver().openInputStream(Uri.parse(mCursor.getString(2)));
Bitmap image = BitmapFactory.decodeStream(in);
iv.setImageBitmap(image);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}`
在本例中,从自定义的ContentProvider
中获得一个托管游标,引用数据的公开 Uri 和列名。然后使用一个SimpleCursorAdapter
将数据连接到一个ListView
,只显示名称值。
当用户点击列表中的任何项目时,光标移动到该位置,相应的名称和图像显示在上面。这是活动调用ContentResolver.openInputStream()
通过存储在列字段中的 Uri 访问素材图像的地方。
Figure 5–4 显示了运行该应用并选择列表中最后一项的结果(Jill Doe)。
图 5–4。 活动从 ContentProvider 提取资源
请注意,到Cursor
的连接没有被显式关闭,因为它是使用managedQuery()
创建的,这意味着活动将把光标作为其正常生命周期的一部分来管理,包括在活动离开前台时关闭它。
需要了解的有用工具:SQLite3
Android 提供了sqlite3
工具(在 Android SDK 主目录的tools
子目录中),用于在您的托管平台上创建新数据库和管理现有数据库,或者(当与 Android 调试桥工具adb
结合使用时)在 Android 设备上创建新数据库和管理现有数据库。如果你不熟悉sqlite3
,将你的浏览器指向[
sqlite.org/sqlite.html](http://sqlite.org/sqlite.html)
,阅读这个命令行工具的简短教程。
您可以用一个数据库文件名参数指定sqlite3
(例如sqlite3 employees
)来创建数据库文件(如果它不存在的话)或者打开现有的文件,并进入这个工具的 shell,从这里您可以执行特定于sqlite3
的点前缀命令和 SQL 语句。如图 Figure 5–5 所示,您也可以不带参数地指定sqlite3
并输入 shell。
图 5–5。 调用sqlite3
时不带数据库文件名参数
Figure 5–5 揭示了进入sqlite3
shell 后欢迎您的开场白,它由您输入命令的sqlite>
提示符指示。当您键入特定于sqlite3
的“.help
命令时,它还会显示部分帮助文本。
**提示:**您可以在没有参数的情况下指定sqlite3
之后创建一个数据库,方法是输入适当的 SQL 语句来创建和填充所需的表(并可能创建索引),然后在退出sqlite3
之前调用.backup*filename*
(其中 filename
标识存储数据库的文件)。
在你的托管平台上创建了数据库之后,你会想把它上传到你的 Android 设备上。您可以根据以下命令行语法,通过使用push
命令调用adb
工具来完成这项任务:
adb [-s <*serialNumber*>] push *local*.db /data/data/<*application package*>/databases/*remote*.db
该命令将标识为 local.db
的本地托管数据库推送到名为 remote.db
的文件中,该文件位于连接的 Android 设备上的/data/data/<*application package*>/databases
目录中。
注意: Local
和 remote
是实际数据库文件名的占位符。按照惯例,文件名与一个.db
文件扩展名相关联(尽管扩展名不是强制性的)。另外,/data/data/<*application package*>
是指应用自己的私有存储区, application package
是指应用唯一的包名。
如果只有一个设备连接到托管平台,则不需要-s <*serialNumber*>
,本地数据库被推送到该设备上。如果连接了多个设备,需要使用-s <*serialNumber*>
来识别特定设备(例如-s emulator-5556
)。
或者,您可能希望将设备的数据库下载到您的托管平台,也许是为了与设备应用的桌面版本一起使用。您可以根据下面的语法通过调用带有pull
命令的adb
来完成这个任务:
adb [-s <*serialNumber*>] pull /data/data/<*application package*>/databases/*remote*.db *local*.db
如果您想使用sqlite3
来管理存储在设备上的 SQLite 数据库,您需要从该设备的adb
远程 shell 中调用这个工具。您可以根据以下语法通过调用adb
和sqlite3
来完成这项任务:
`adb [-s <serialNumber >] shell
sqlite3 /data/data/<application package >/databases/remote .db`
adb
外壳由#
提示符指示。输入sqlite3
,后跟现有设备托管的数据库文件的路径和名称,以操作数据库,或创建新的数据库。或者,您可以不带参数地输入sqlite3
。
sqlite3
命令呈现了与您在图 5–1 中看到的相同的序言。输入sqlite3
命令,发出 SQL 语句来管理 remote.db
(或者创建一个新的数据库),然后退出sqlite3
( .exit
或者.quit
),再退出adb
shell ( exit
)。
SQLite3 和 UC
第一章向你介绍了一个名为UC
的应用。这个单位转换应用允许你在不同的单位之间进行转换(例如,从华氏温度到摄氏温度)。
虽然很有用,UC
也有缺陷,因为每次有新的转换添加到它的转换列表中时,都必须重新构建它。我们可以通过在数据库中存储UC
的转换来消除这个缺陷,这就是我们在本节中要做的。
我们将首先创建一个数据库来存储转换列表。数据库将由一个带有conversion
和multiplier
列的conversions
表组成。此外,数据库将存储在一个conversions.db
文件中。
Table 5–1 列出了将存储在conversion
和multiplier
列中的值。
表 5–1。 列Conversion
和Multiplier
列 的值
| **转换** | 乘法器 |
| :-- | :-- |
| 英亩到平方英里 | 0.0015625 |
| 大气压至帕斯卡 | One hundred and one thousand three hundred and twenty-five |
| 巴到帕斯卡 | One hundred thousand |
| 摄氏温度到华氏温度 | 0(占位符) |
| 华氏度到摄氏度 | 0(占位符) |
| 达因到牛顿 | 0.00001 |
| 英尺/秒到米/秒 | 0.3048 |
| 液体盎司(英国)到升 | 0.0284130625 |
| 液体盎司(美国)到升 | 0.0295735295625 |
| 马力(电力)至瓦特 | Seven hundred and forty-six |
| 马力(公制)到瓦特 | Seven hundred and thirty-five point four nine nine |
| 千克到吨(英制或长制) | 1/1016.0469088 |
| 千克到吨(美制或短制) | 1/907.18474 |
| 升到液体盎司(英国) | 1/0.0284130625 |
| 升到液体盎司(美国) | 1/0.0295735295625 |
| 马赫数到米/秒 | Three hundred and thirty-one point five |
| 米/秒到英尺/秒 | 1/0.3048 |
| 米/秒到马赫数 | 1/331.5 |
| 英里/加仑(英国)到英里/加仑(美国) | Zero point eight three three |
| 英里/加仑(美国)到英里/加仑(英国) | 1/0.833 |
| 牛顿至达因 | One hundred thousand |
| 帕斯卡至大气压 | 1/101325.0 |
| 帕斯卡到巴 | 0.00001 |
| 平方英里到英亩 | Six hundred and forty |
| 吨(英制或长制)到千克 | 1016.0469088 |
| 吨(美制或短制)到千克 | 907.18474 |
| 瓦特/马力 _ 电动) | 1/746.0 |
| 瓦特对马力(公制) | 1/735.499 |
在命令行执行sqlite3 conversions.db
创建conversions.db
并进入 shell,然后执行 SQL 语句create table conversions(conversion varchar(50), mutliplier float);
创建该数据库的conversions
表。
继续,输入一系列 insert 语句,将表 5–1 的值行插入到conversions
中。例如,SQL 语句insert into conversions values('Acres to square miles', 0.0015625);
将第一行的值插入到表中。
**注意:**您必须按照它们在表 5–1 中出现的顺序插入行,因为Degrees Celsius to Degrees Fahrenheit
和Degrees Fahrenheit to Degrees Celsius
必须出现在从零开始的位置 3 和 4,因为这些位置在UC2.java
中是硬编码的。
接下来,我们将创建一个类似于UC
的UC2
应用,但是从conversions.db
获得它的转换。按照第一章的秘籍 1-10(用 Eclipse 开发UC
)中的说明完成这项任务,但要做以下更改(参见清单 5–30):
将包名从com.apress.uc
更改为com.apress.uc2
。
忽略arrays.xml
文件。UC2
不需要这个文件。
用清单 5–26 替换框架UC2.java
源代码。
清单 5–30。 执行从Conversions.db
获取的单位换算的活动
`public class UC2 extends Activity {
private int position = 0;
private String[] conversions;
private double[] multipliers;
private class DBHelper extends SQLiteOpenHelper
{
private final static String DB_PATH = "data/data/com.apress.uc2/databases/";
private final static String DB_NAME = "conversions.db";
private final static int CONVERSIONS_COLUMN_ID = 0;
private final static int MULTIPLIERS_COLUMN_ID = 1;
private SQLiteDatabase db;
public DBHelper(Context context)
{
super(context, DB_NAME, null, 1);
}
@Override
public void onCreate(SQLiteDatabase db)
{
// Do nothing ... we don't create a new database.
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldver, int newver)
{
// Do nothing ... we don't upgrade a database.
}
public boolean populateArrays()
{
try
{
String path = DB_PATH+DB_NAME;
db = SQLiteDatabase.openDatabase(path, null, SQLiteDatabase.OPEN_READONLY|
SQLiteDatabase.NO_LOCALIZED_COLLATORS);
Cursor cur = db.query("conversions", null, null, null, null, null, null);
if (cur.getCount() == 0)
{
Toast.makeText(UC2.this, "conversions table is empty",
Toast.LENGTH_LONG).show();
return false;
}
conversions = new String[cur.getCount()];
multipliers = new double[cur.getCount()];
int i = 0;
while (cur.moveToNext())
{
conversions[i] = cur.getString(CONVERSIONS_COLUMN_ID);
multipliers[i++] = cur.getFloat(MULTIPLIERS_COLUMN_ID);
}
return true;
}
catch (SQLException sqle)
{
Toast.makeText(UC2.this, sqle.getMessage(), Toast.LENGTH_LONG).show();
}
finally
{
if (db != null)
db.close();
}
return false;
}
}
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
DBHelper dbh = new DBHelper(this);
if (!dbh.populateArrays())
finish();
final EditText etUnits = (EditText) findViewById(R.id.units);
final Spinner spnConversions = (Spinner) findViewById(R.id.conversions);
ArrayAdapter aa;
aa = new ArrayAdapter(this, android.R.layout.simple_spinner_item,
conversions);
aa.setDropDownViewResource(android.R.layout.simple_spinner_item);
spnConversions.setAdapter(aa);
AdapterView.OnItemSelectedListener oisl;
oisl = new AdapterView.OnItemSelectedListener()
{
@Override
public void onItemSelected(AdapterView<?> parent, View view,
int position, long id)
@Override
public void onNothingSelected(AdapterView<?> parent)
{
System.out.println("nothing");
}
};
spnConversions.setOnItemSelectedListener(oisl);
final Button btnClear = (Button) findViewById(R.id.clear);
AdapterView.OnClickListener ocl;
ocl = new AdapterView.OnClickListener()
{
@Override
public void onClick(View v)
{
etUnits.setText("");
}
};
btnClear.setOnClickListener(ocl);
btnClear.setEnabled(false);
final Button btnConvert = (Button) findViewById(R.id.convert);
ocl = new AdapterView.OnClickListener()
{
@Override
public void onClick(View v)
{
String text = etUnits.getText().toString();
double input = Double.parseDouble(text);
double result = 0;
if (position == 3)
result = input*9.0/5.0+32; // Celsius to Fahrenheit
else
if (position == 4)
result = (input-32)5.0/9.0; // Fahrenheit to Celsius
else
result = input multipliers[position];
etUnits.setText(""+result);
}
};
btnConvert.setOnClickListener(ocl);
btnConvert.setEnabled(false);
Button btnClose = (Button) findViewById(R.id.close);
ocl = new AdapterView.OnClickListener()
{
@Override
public void onClick(View v)
{
finish();
}
};
btnClose.setOnClickListener(ocl);
TextWatcher tw;
tw = new TextWatcher()
{
public void afterTextChanged(Editable s)
public void beforeTextChanged(CharSequence s, int start, int count,
int after)
public void onTextChanged(CharSequence s, int start, int before,
int count)
{
if (etUnits.getText().length() == 0)
{
btnClear.setEnabled(false);
btnConvert.setEnabled(false);
}
else
{
btnClear.setEnabled(true);
btnConvert.setEnabled(true);
}
}
};
etUnits.addTextChangedListener(tw);
}
}`
UC2
与UC
的不同之处主要在于依靠DBHelper
内部类从conversions.db
数据库的conversions
表中的conversion
和multiplier
列获取其conversions
和multipliers
数组的值。
DBHelper
扩展android.database.sqlite.SQLiteOpenHelper
并覆盖它的抽象onCreate()
和onUpgrade()
方法。重写方法什么都不做;重要的是数据库是否可以打开。
数据库以populateArrays()
方法打开。如果成功打开,查询conversions
表以返回所有行。如果返回的android.database.Cursor
对象包含至少一行,数组由Cursor
值填充。
如果出现问题,会显示一条提示消息。虽然对于这个简单的例子来说很方便,但是您可能希望显示一个对话框并将其字符串存储在资源文件中。无论是否显示 toast 消息,数据库都是关闭的。
UC2
与UC
的区别还在于,它直接实例化了android.widget.ArrayAdapter
,而不是调用这个类的createFromResource()
方法。这样做是为了将字符串名称的conversions
数组传递给ArrayAdapter
实例。
假设您已经构建了这个应用,从 Eclipse 启动它。UC2
将短暂显示一个空白屏幕,然后在结束前显示一条提示信息。图 5–6 显示这个吐司出现在应用启动屏幕上。
图 5–6。 因为conversions.db
数据库还没有出现在设备上,所以显示了一个 toast。
出现 toast 是因为在/data/data/com.apress.uc2/databases/
路径中不存在conversions.db
数据库。我们可以通过将之前创建的conversions.db
文件上传到这个路径来纠正这种情况,如下所示:
adb push conversions.db /data/data/com.apress.uc2/databases/conversions.db
这一次,当您启动该应用时,您应该会看到出现在 Figure 5–7 中的屏幕。
图 5–7。 单位转换器的单独屏幕让你执行各种单位转换。
遭受以下一对缺陷的困扰——考虑将修复这些缺陷作为要完成的练习:
由于在UC2.java
中对这些位置进行了硬编码,因此Degrees Celsius to Degrees Fahrenheit
和Degrees Fahrenheit to Degrees Celsius
转换必须出现在从零开始的位置 3 和 4。这个硬编码位于下面的摘录中,该摘录摘自在清单 5–30 中分配给点击按钮的点击监听器中的onClick()
方法:if (position == 3) result = input*9.0/5.0+32; // Celsius to Fahrenheit else if (position == 4) result = (input-32)*5.0/9.0; // Fahrenheit to Celsius else result = input*multipliers[position];
DBHelper
的populateArrays()
方法填充应用主线程上的conversions
和multipliers
数组。这应该不成问题,因为conversions
表只包含 28 行。然而,如果向该表中添加更多的行,主线程可能会被占用足够长的时间,以至于可怕的应用不响应 对话框出现(参见附录 C)。此外,这也是为什么 Android 文档指出SQLiteOpenHelper
的getReadableDatabase()
和getWritableDatabase()
方法不应该在主线程上调用的原因。然而,对于小型数据库,在主线程上调用这些方法应该不成问题。
总结
在这一章中,你已经研究了许多在 Android 设备上持久化数据的实用方法。您了解了如何快速创建首选项屏幕,以及如何使用首选项和简单的方法来持久化基本数据类型。您已经看到了文件的放置方式和位置,既可用于参考,也可用于存储。您甚至学习了如何与其他应用共享持久化数据。在下一章中,我们将研究如何利用操作系统的服务来进行后台操作和应用之间的通信。
六、与系统交互
Android 操作系统提供了许多应用可以利用的有用服务。这些服务中的许多都是为了让您的应用能够在移动系统中运行,而不仅仅是与用户进行短暂的交互。应用可以为自己安排警报,运行后台服务,并相互发送消息;所有这些都允许 Android 应用最大程度地与移动设备集成。此外,Android 提供了一套标准接口,旨在向您的软件公开其核心应用收集的所有数据。通过这些接口,任何应用都可以集成、添加和改进平台的核心功能,从而增强用户体验。
6–1。从后台通知
问题
您的应用在后台运行,当前没有对用户可见的界面,但必须通知用户发生了重要事件。
解决方案
(API 一级)
使用NotificationManager
发布状态栏通知。Notifications
是一种不引人注目的方式,告诉用户你想引起他们的注意。也许新消息已经到达,更新可用,或者长时间运行的作业已经完成;Notifications
非常适合完成所有这些任务。
工作原理
一个Notification
可以从任何系统组件发送到NotificationManager
,比如一个服务、广播接收器或活动。在这个例子中,我们将看到一个使用延迟来模拟长时间运行的操作的活动,当它完成时会产生一个Notification
。
清单 6–1。 活动触发通知
`public class NotificationActivity extends Activity implements View.OnClickListener {
private static final intNOTE_ID = 100;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Button button = new Button(this);
button.setText("Post New Notification");
button.setOnClickListener(this);
setContentView(button);
}
@Override
public void onClick(View v) {
//Run 10 seconds after click
handler.postDelayed(task, 10000);
Toast.makeText(this, "Notification will post in 10 seconds", Toast.LENGTH_SHORT).show();
}
private Handler handler = new Handler();
private Runnable task = new Runnable() {
@Override
public void run() {
NotificationManager manager =
(NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
Intent launchIntent = new Intent(getApplicationContext(), NotificationActivity.class);
PendingIntent contentIntent =
PendingIntent.getActivity(getApplicationContext(), 0, launchIntent, 0);
//Create notification with the time it was fired
Notification note = new Notification(R.drawable.icon, "Something Happened",
System.currentTimeMillis());
//Set notification information
note.setLatestEventInfo(getApplicationContext(), "We're Finished!", "Click Here!", contentIntent);
note.defaults |= Notification.DEFAULT_SOUND;
note.flags |= Notification.FLAG_AUTO_CANCEL;
manager.notify(NOTE_ID, note);
}
};
}`
这个例子使用了一个Handler
来调度一个任务,通过调用按钮监听器中的Handler.postDelayed()
在按钮被点击十秒钟后发送Notification
。不管活动是否在前台,这个任务都会执行,所以如果用户厌倦了并离开应用,他们仍然会得到通知。
当计划任务执行时,会创建一个新的通知。可以提供图标资源和标题字符串,这些项目将在通知发生时显示在状态栏中。此外,我们传递一个时间值(以毫秒为单位)作为事件时间显示在通知列表中。这里,我们将该值设置为通知触发的时间,但是在您的应用中它可能有不同的含义。
一旦Notification
被创建,我们用一些有用的参数填充它。使用Notification.setLatestEventInfo()
,当用户下拉状态栏时,我们在通知列表中显示更详细的文本。
传递给这个方法的参数之一是一个指向我们活动的PendingIntent
。这种意图使得通知是交互式的,允许用户点击列表中的通知并启动活动。
**注意:**这个意向将为每个事件发起一个新的活动。如果您希望活动的现有实例响应启动,如果堆栈中存在一个实例,请确保适当地包含意图标志和清单参数来实现这一点,例如Intent.FLAG_ACTIVITY_CLEAR_TOP
和android:launchMode="singleTop."
为了增强状态栏中视觉动画之外的Notification
,修改了Notification.defaults
位掩码,以包括当Notification
触发时系统默认的通知声音。也可以添加诸如Notification.DEFAULT_VIBRATION
和Notification.DEFAULT_LIGHTS
的值。
**提示:**如果您想定制用Notification
播放的声音,将Notification.sound
参数设置为引用文件的Uri
或要读取的ContentProvider
。
向Notification.flags
位掩码添加一系列标志允许进一步定制Notification
。这个例子使Notification.FLAG_AUTO_CANCEL
能够表示一旦用户选择了通知,就应该取消通知,或者从列表中删除。如果没有此标志,通知将保留在列表中,直到通过调用NotificationManager.cancel()
或NotificationManager.cancelAll()
手动取消。
以下是其他一些有用的标志:
FLAG_INSISTENT
重复Notification
声音,直到用户做出响应。
FLAG_NO_CLEAR
不允许用用户的“清除通知”按钮清除Notification
;只能通过调用cancel()
。
一旦通知准备好了,就用NotificationManager.notify()
发送给用户,它也带有一个 ID 参数。应用中的每个Notification
类型都应该有一个惟一的 ID。管理器一次只允许列表中有一个具有相同 ID 的Notification
,具有相同 ID 的新实例将取代现有的实例。另外,手动取消特定的Notification
需要 ID。
当我们运行这个例子时,像 Figure 6–1 这样的活动向用户显示一个按钮。按下按钮后,您可以在一段时间后看到通知帖子,即使该活动不再可见(参见图 6–2)。
图 6–1。 通知从按钮按下 开始张贴
图 6–2。 通知发生(左),并显示在 列表中(右)
6–2。创建定时和周期性任务
问题
您的应用需要在计时器上运行一个操作,比如定期更新 UI。
解决方案
(API 一级)
使用由Handler
提供的定时操作。有了Handler
,可以有效地安排操作在特定的时间发生,或者在指定的延迟之后发生。
它是如何工作的
让我们看一个在TextView
中显示当前时间的示例活动。参见清单 6–2。
清单 6–2。 用处理程序更新的活动
`public class TimingActivity extends Activity {
TextView mClock;`
` @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mClock = new TextView(this);
setContentView(mClock);
}
private Handler mHandler = new Handler();
private Runnable timerTask = new Runnable() {
@Override
public void run() {
Calendar now = Calendar.getInstance();
mClock.setText(String.format("%02d:%02d:%02d",
now.get(Calendar.HOUR),
now.get(Calendar.MINUTE),
now.get(Calendar.SECOND)) );
//Schedule the next update in one second
mHandler.postDelayed(timerTask,1000);
}
};
@Override
public void onResume() {
super.onResume();
mHandler.post(timerTask);
}
@Override
public void onPause() {
super.onPause();
mHandler.removeCallbacks(timerTask);
}
}`
这里我们已经将读取当前时间和更新 UI 的操作封装到一个名为timerTask
的Runnable
中,它将由已经创建的Handler
触发。当活动变得可见时,调用Handler.post()
尽快执行任务。在更新了TextView
之后,timerTask
的最后一个操作是调用处理程序,使用Handler.postDelayed()
来调度从现在起一秒钟(1000 毫秒)后的另一次执行。
只要活动没有中断,这个循环就会继续,UI 每秒都会更新。一旦活动暂停(用户离开或其他事情吸引了他们的注意力),Handler.removeCallbacks()
删除所有挂起的操作,并确保任务不会被进一步调用,直到活动再次可见。
**提示:**在这个例子中,我们更新 UI 是安全的,因为Handler
是在主线程上创建的。操作将总是在发布它们的Handler
所连接的同一个线程上执行。
6–3。计划周期性任务
问题
您的应用需要注册才能定期运行任务,例如检查服务器的更新或提醒用户做一些事情。
解决方案
(API 一级)
利用AlarmManager
来管理和执行你的任务。AlarmManager
对于调度未来的单个或重复操作非常有用,即使您的应用没有运行,这些操作也需要发生。每当闹钟设定好的时候,AlarmManager
就会被交给一个PendingIntent
去启动。这个意图可以指向任何系统组件,比如一个Activity
、BroadcastReceiver
或Service
,当警报触发时执行。
应该注意的是,这种方法最适合于即使在应用代码可能没有运行时也需要发生的操作。AlarmManager
需要太多的开销,对于在应用使用时可能需要的简单计时操作来说是无用的。使用Handler
的postAtTime()
和postDelayed()
方法可以更好地处理这些问题。
它是如何工作的
让我们看看如何使用AlarmManager
定期触发广播接收器。参见清单 6–3 到清单 6–5。
清单 6–3。 待触发的广播接收器
public class AlarmReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { //Perform an interesting operation, we'll just display the current time Calendar now = Calendar.getInstance(); DateFormat formatter = SimpleDateFormat.getTimeInstance(); Toast.makeText(context, formatter.format(now.getTime()), Toast.LENGTH_SHORT).show(); } }
**提醒:**必须在清单中用一个<receiver>
标签声明一个 BroadcastReceiver ( AlarmReceiver
),以便AlarmManager
能够触发它。确保在您的<application>
标签中包含一个,如下所示:
<application> … <receiver android:name=".AlarmReceiver"></receiver> </application>
清单 6–4。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <Button android:id="@+id/start" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Start Alarm" /> <Button android:id="@+id/stop" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Cancel Alarm" /> </LinearLayout>
清单 6–5。 注册/注销报警的活动
`public class AlarmActivity extends Activity implements View.OnClickListener {
private PendingIntent mAlarmIntent;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//Attach the listener to both buttons
findViewById(R.id.start).setOnClickListener(this);
findViewById(R.id.stop).setOnClickListener(this);
//Create the launch sender
Intent launchIntent = new Intent(this, AlarmReceiver.class);
mAlarmIntent = PendingIntent.getBroadcast(this, 0, launchIntent, 0);
}
@Override
public void onClick(View v) {
AlarmManager manager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
long interval = 5*1000; //5 seconds
switch(v.getId()) {
case R.id.start:
Toast.makeText(this, "Scheduled", Toast.LENGTH_SHORT).show();
manager.setRepeating(AlarmManager.ELAPSED_REALTIME,
SystemClock.elapsedRealtime()+interval,
interval,
mAlarmIntent);
break;
case R.id.stop:
Toast.makeText(this, "Canceled", Toast.LENGTH_SHORT).show();
manager.cancel(mAlarmIntent);
break;
default:
break;
}
}
}`
在这个例子中,我们提供了一个非常基本的 BroadcastReceiver,当它被触发时,会简单地将当前时间显示为 Toast。该接收器必须用一个<receiver>
标签在应用的清单中注册。否则,应用外部的AlarmManager
将不会知道如何触发它。示例活动提供了两个按钮:一个开始触发常规警报,另一个取消警报。
要触发的操作由PendingIntent
引用,它将用于设置和取消报警。我们创建一个直接引用应用的 BroadcastReceiver 的 Intent,然后使用getBroadcast()
从该 Intent 创建一个PendingIntent
(因为我们正在创建一个对 BroadcastReceiver 的引用)。
提醒: PendingIntent
有创建者方法getActivity()
也有getService()
。确保在创建这个部分时引用正确的应用组件。
当按下开始按钮时,活动使用AlarmManager.setRepeating()
记录一个重复报警。除了 PendingIntent 之外,该方法还需要一些参数来确定何时触发警报。第一个参数根据使用的时间单位以及设备处于睡眠模式时是否应该发出警报来定义警报类型。在本例中,我们选择了ELAPSED_REALTIME
,它表示自上次设备启动以来的值(单位为毫秒)。此外,还有三种其他模式可供使用:
ELAPSED_REALTIME_WAKEUP
报警时间是指经过的时间,如果设备处于睡眠状态,将唤醒设备触发报警。
RTC
RTC_WAKEUP
参考 UTC 时间的闹钟时间,如果设备处于睡眠状态,将唤醒设备进行触发。
以下参数(分别)指的是警报第一次触发的时间和重复的时间间隔。因为选择的警报类型是 ELAPSED_REALTIME,开始时间也必须相对于经过时间;SystemClock.elapsedRealtime()
以此格式提供当前时间。
示例中的警报被注册为在按下按钮 5 秒后触发,之后每隔 5 秒触发一次。每五秒钟,屏幕上会出现一个带有当前时间值的Toast
,即使该应用不再运行或不在用户面前。当用户显示活动并按下停止按钮时,任何与我们的PendingIntent
匹配的未决警报都会被立即取消…停止Toast
的流程
一个更精确的例子
如果我们想安排一个闹铃在特定的时间发生呢?也许每天早上 9 点一次?用一些稍微不同的参数设置AlarmManager
可以实现这一点。参见清单 6–6。
清单 6–6。 精确报警
` long oneDay = 243600 1000; //24 hours
long firstTime;
//Get a Calendar (defaults to today)
//Set the time to 09:00:00
Calendar startTime = Calendar.getInstance();
startTime.set(Calendar.HOUR_OF_DAY, 9);
startTime.set(Calendar.MINUTE, 0);
startTime.set(Calendar.SECOND, 0);
//Get a Calendar at the current time
Calendar now = Calendar.getInstance();
if(now.before(startTime)) {
//It's not 9AM yet, start today
firstTime = startTime.getTimeInMillis();
} else {
//Start 9AM tomorrow
startTime.add(Calendar.DATE, 1);
firstTime = startTime.getTimeInMillis();
}
//Set the alarm
manager.setRepeating(AlarmManager.RTC_WAKEUP,
firstTime,
oneDay,
mAlarmIntent);`
这个例子使用了一个实时报警。确定上午 9:00 的下一次发生是在今天还是明天,并且返回该值作为警报的初始触发时间。然后,以毫秒为单位的 24 小时的计算值作为时间间隔,这样从该时间点开始,每天触发一次警报。
**重要提示:**警报不会在设备重启后持续存在。如果设备关闭后又重新打开,则必须重新安排任何先前注册的警报。
6–4 岁。创建粘性操作
问题
您的应用需要执行一个或多个后台操作,即使用户暂停应用,这些操作也会运行到完成。
解决方案
(API 三级)
创建一个IntentService
的实现来处理这项工作。IntentService
是 Android 基础服务实现的包装器,是在后台工作而无需用户交互的关键组件。IntentService
对传入的工作进行排队(用 Intents 表示),依次处理每个请求,然后在队列为空时自行停止。
IntentService
还处理后台工作所需的工作线程的创建,因此不必使用 AsyncTask 或 Java 线程来确保操作在后台正常进行。
这个菜谱研究了一个使用IntentService
创建后台操作的中央管理器的例子。在本例中,将通过调用Context.startService()
从外部调用管理器。经理会将收到的所有请求排队,并通过给onHandleIntent()
打电话来单独处理它们。
它是如何工作的
让我们来看看如何构造一个简单的IntentService
实现来处理一系列后台操作。参见清单 6–7。
清单 6–7。 IntentService 搬运操作
`public class OperationsManager extends IntentService {
public static final String ACTION_EVENT = "ACTION_EVENT";
public static final String ACTION_WARNING = "ACTION_WARNING";
public static final String ACTION_ERROR = "ACTION_ERROR";
public static final String EXTRA_NAME = "eventName";
private static final String LOGTAG = "EventLogger";
private IntentFilter matcher;`
` public OperationsManager() {
super("OperationsManager");
//Create the filter for matching incoming requests
matcher = new IntentFilter();
matcher.addAction(ACTION_EVENT);
matcher.addAction(ACTION_WARNING);
matcher.addAction(ACTION_ERROR);
}
@Override
protectedvoid onHandleIntent(Intent intent) {
//Check for a valid request
if(!matcher.matchAction(intent.getAction())) {
Toast.makeText(this, "OperationsManager: Invalid Request", Toast.LENGTH_SHORT).show();
return;
}
//Handle each request directly in this method. Don't create more threads.
if(TextUtils.equals(intent.getAction(), ACTION_EVENT)) {
logEvent(intent.getStringExtra(EXTRA_NAME));
}
if(TextUtils.equals(intent.getAction(), ACTION_WARNING)) {
logWarning(intent.getStringExtra(EXTRA_NAME));
}
if(TextUtils.equals(intent.getAction(), ACTION_ERROR)) {
logError(intent.getStringExtra(EXTRA_NAME));
}
}
private void logEvent(String name) {
try {
//Simulate a long network operation by sleeping
Thread.sleep(5000);
Log.i(LOGTAG, name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void logWarning(String name) {
try {
//Simulate a long network operation by sleeping
Thread.sleep(5000);
Log.w(LOGTAG, name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void logError(String name) {
try {
//Simulate a long network operation by sleeping
Thread.sleep(5000);
Log.e(LOGTAG, name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}`
注意IntentService
没有默认的构造函数(没有参数),所以自定义实现必须实现一个构造函数,用服务名调用 super。这个名称在技术上没有什么重要性,因为它只对调试有用;Android 使用提供的名称来命名它创建的工作线程。
服务通过onHandleIntent()
方法处理所有请求。这个方法是在提供的 worker 线程上调用的,所以所有的工作都应该直接在这里完成;不应创建新的线程或操作。当onHandleIntent()
返回时,这是 IntentService 开始处理队列中下一个请求的信号。
这个示例提供了三个日志记录操作,可以在请求意图上使用不同的操作字符串来请求这些操作。出于演示目的,每个操作都使用特定的日志记录级别(信息、警告或错误)将提供的消息写入设备日志。请注意,消息本身是作为请求意图的额外内容传递的。使用每个意图的数据和额外字段来保存操作的任何参数,让操作字段来定义操作类型。
示例服务维护一个 IntentFilter,它用于方便地确定是否发出了有效的请求。当创建服务时,所有有效的动作都被添加到过滤器中,允许我们对任何传入的请求调用IntentFilter.matchAction()
来确定它是否包括我们可以在这里处理的动作。
清单 6–8 是一个调用这个服务来执行工作的活动的例子。
清单 6–8。 AndroidManifest.xml
`
`
**提醒:**Android manifest . XML 中的package
属性必须与您为应用选择的包相匹配;"com.examples.sticky"
只是我们在这里的例子中选择的包。
**注意:**因为IntentService
是作为服务调用的,所以必须使用<service>
标签在应用清单中声明它。
清单 6–9。 活动调用 IntentService
`public class ReportActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
logEvent("CREATE");
}
@Override
public void onStart() {
super.onStart();
logEvent("START");
}
@Override
public void onResume() {
super.onResume();
logEvent("RESUME");
}
@Override
public void onPause() {
super.onPause();
logWarning("PAUSE");
}
@Override
public void onStop() {
super.onStop();
logWarning("STOP");
}
@Override
public void onDestroy() {
super.onDestroy();
logWarning("DESTROY");
}
private void logEvent(String event) {
Intent intent = new Intent(this, OperationsManager.class);
intent.setAction(OperationsManager.ACTION_EVENT);
intent.putExtra(OperationsManager.EXTRA_NAME, event);
startService(intent);
}
private void logWarning(String event) {
Intent intent = new Intent(this, OperationsManager.class);
intent.setAction(OperationsManager.ACTION_WARNING);
intent.putExtra(OperationsManager.EXTRA_NAME, event);
startService(intent);
}
}`
这个活动没什么好看的,因为所有有趣的事件都是通过设备日志发送出去的,而不是发送到用户界面。然而,它有助于说明我们在前一个示例中创建的服务的队列处理行为。当活动变得可见时,它将调用所有正常的生命周期方法,导致对日志服务的三个请求。在处理每个请求时,日志中将输出一行,服务将继续。
提示 :这些日志语句可以通过 SDK 提供的logcat
工具看到。从大多数开发环境(包括 Eclipse)中都可以看到来自设备或仿真器的logcat
输出,或者通过在命令行键入adblogcat.
就可以看到
还要注意,当服务完成所有三个请求时,系统会在日志中发出通知,指出服务已经停止。仅在完成作业所需的时间内存中存在;这是一个非常有用的特性,让你的服务成为系统的好公民。
按下 HOME 或 BACK 按钮将导致更多的生命周期方法生成服务请求,并注意暂停/停止/销毁部分调用服务中的单独操作,导致它们的消息被记录为警告;简单地将请求意图的动作字符串设置为不同的值就可以控制这一点。
请注意,即使应用不再可见(或者打开了另一个应用),消息仍会继续输出到日志中。这就是 Android 服务组件的强大之处。无论用户行为如何,这些操作在完成之前都会受到系统保护。
可能的缺点
在每种操作方法中,都设置了五秒钟的延迟,以模拟发出远程 API 或一些类似操作的实际请求所需的时间。当运行这个例子时,它也有助于说明IntentService
用单个工作线程以串行方式处理发送给它的所有请求。该示例对来自每个生命周期方法的多个连续请求进行排队,但是结果仍然是每五秒钟一条日志消息,因为 IntentService 在当前请求完成之前不会启动一个新请求(实际上是在onHandleIntent()
返回时)。
如果您的应用需要粘性后台任务的并发性,您可能需要创建一个更加定制的服务实现,使用线程池来执行工作。Android 开源的美妙之处在于,如果需要的话,你可以直接找到IntentService
的源代码,并将其作为实现的起点,从而最大限度地减少所需的时间和定制代码。
6–5 岁。运行持久的后台操作
问题
您的应用有一个组件,它必须在后台无限期运行,执行某些操作或监视某些事件的发生。
解决方案
(API 一级)
将组件构建成服务。服务被设计为后台组件,应用可以启动这些组件并让它们无限期地运行。就防止在内存不足的情况下被终止而言,服务还被赋予了高于其他后台进程的更高的地位。
对于不需要直接连接到另一个组件的操作(如活动),可以显式地启动和停止服务。但是,如果应用必须直接与服务交互,则提供一个绑定接口来传递数据。在这些情况下,服务可以由系统隐式地启动和停止,这是实现其所请求的绑定所需要的。
对于服务实现,要记住的关键是始终保持用户友好。除非用户明确要求,否则不确定操作很可能不应该启动。整个应用可能应该包含一个界面或设置,允许用户控制启用或禁用这样的服务。
它是如何工作的
清单 6–10 是一个持久化服务的例子,用于在一定时期内跟踪和记录用户的位置。
清单 6–10。 持久跟踪服务
`public class TrackerService extends Service implements LocationListener {
private static final String LOGTAG = "TrackerService";
private LocationManager manager;
private ArrayList storedLocations;`
` privateboolean isTracking = false;
/* Service Setup Methods */
@Override
public void onCreate() {
manager = (LocationManager)getSystemService(LOCATION_SERVICE);
storedLocations = new ArrayList();
Log.i(LOGTAG, "Tracking Service Running...");
}
@Override
public void onDestroy() {
manager.removeUpdates(this);
Log.i(LOGTAG, "Tracking Service Stopped...");
}
public void startTracking() {
if(!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
return;
}
Toast.makeText(this, "Starting Tracker", Toast.LENGTH_SHORT).show();
manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 30000, 0, this);
isTracking = true;
}
public void stopTracking() {
Toast.makeText(this, "Stopping Tracker", Toast.LENGTH_SHORT).show();
manager.removeUpdates(this);
isTracking = false;
}
publicboolean isTracking() {
return isTracking;
}
/* Service Access Methods */
public class TrackerBinder extends Binder {
TrackerService getService() {
return TrackerService.this;
}
}
private final IBinder binder = new TrackerBinder();
@Override
public IBinder onBind(Intent intent) {
return binder;
}
publicint getLocationsCount() {
return storedLocations.size();
}
public ArrayList getLocations() {
return storedLocations;
}
/* LocationListener Methods */
@Override
public void onLocationChanged(Location location) {
Log.i("TrackerService", "Adding new location");
storedLocations.add(location);
}
@Override
public void onProviderDisabled(String provider)
@Override
public void onProviderEnabled(String provider)
@Override
public void onStatusChanged(String provider, int status, Bundle extras)
}`
该服务的工作是监控和跟踪它从LocationManager
接收的更新。当创建服务时,它准备一个空白的Location
条目列表,并等待开始跟踪。一个外部组件,比如一个活动,可以调用startTracking()
和stopTracking()
来启用和禁用位置更新到服务的流程。此外,还公开了访问服务已记录的位置列表的方法。
因为这个服务需要来自活动或其他组件的直接交互,所以需要一个 Binder 接口。当服务必须跨越流程边界进行通信时,绑定器的概念可能会变得复杂,但是对于像这样的情况,所有东西都位于同一个流程的本地,使用一个方法getService()
创建一个非常简单的绑定器,将服务实例本身返回给调用者。我们稍后将从活动的角度对此进行更详细的讨论。
当在服务上启用跟踪时,它向LocationManager
注册更新,并将收到的每个更新存储在其位置列表中。请注意,调用requestLocationUpdates()
的最短时间为 30 秒。由于这项服务预计将运行很长时间,谨慎的做法是留出更新时间,让 GPS(以及电池)休息一会儿。
现在让我们来看一个允许用户访问该服务的简单活动。参见清单 6–11 至清单 6–13。
清单 6–11。 AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.examples.service" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="1" /> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".ServiceActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".TrackerService"></service> </application> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/> </manifest>
**提醒:**服务必须在应用清单中使用<service>
标签声明,这样 Android 就知道如何以及在哪里调用它。此外,对于本例,权限android.permission.ACCESS_FINE_LOCATION
是必需的,因为我们正在使用 GPS。
清单 6–12。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <Button android:id="@+id/enable" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Start Tracking" /> <Button android:id="@+id/disable" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Stop Tracking" /> <TextView android:id="@+id/status" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout>
清单 6–13。 活动与服务交互
`public class ServiceActivity extends Activity implements View.OnClickListener {
Button enableButton, disableButton;
TextView statusView;
TrackerService trackerService;
Intent serviceIntent;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);`
` enableButton = (Button)findViewById(R.id.enable);
enableButton.setOnClickListener(this);
disableButton = (Button)findViewById(R.id.disable);
disableButton.setOnClickListener(this);
statusView = (TextView)findViewById(R.id.status);
serviceIntent = new Intent(this, TrackerService.class);
}
@Override
public void onResume() {
super.onResume();
//Starting the service makes it stick, regardless of bindings
startService(serviceIntent);
//Bind to the service
bindService(serviceIntent, serviceConnection, Context.BIND_AUTO_CREATE);
}
@Override
public void onPause() {
super.onPause();
if(!trackerService.isTracking()) {
//Stopping the service let's it die once unbound
stopService(serviceIntent);
}
//Unbind from the service
unbindService(serviceConnection);
}
@Override
public void onClick(View v) {
switch(v.getId()) {
case R.id.enable:
trackerService.startTracking();
break;
case R.id.disable:
trackerService.stopTracking();
break;
default:
break;
}
updateStatus();
}
private void updateStatus() {
if(trackerService.isTracking()) {
statusView.setText(
String.format("Tracking enabled. %d locations logged.",trackerService.getLocationsCount()));
} else {
statusView.setText("Tracking not currently enabled.");
}
}
private ServiceConnection serviceConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
trackerService = ((TrackerService.TrackerBinder)service).getService();
updateStatus();
}
public void onServiceDisconnected(ComponentName className)
};
}`
Figure 6–3 显示了用户启用和禁用位置跟踪行为的两个按钮,以及当前服务状态的文本显示。
图 6–3。 服务活动布局
当活动可见时,它被绑定到TrackerService
。这是在ServiceConnection
接口的帮助下完成的,该接口在绑定和解除绑定操作完成时提供回调方法。将服务绑定到活动后,我们现在可以直接调用服务公开的所有公共方法。
然而,单靠绑定无法让服务长期运行;仅通过绑定器接口访问服务会导致服务随着活动的生命周期自动创建和销毁。在这种情况下,我们希望服务持续到该活动在内存中之后。为了实现这一点,服务在绑定之前通过startService()
显式启动。向已经运行的服务发送启动命令没有坏处,所以我们也可以在onResume()
中安全地这样做。
服务现在将继续在内存中运行,即使在活动解除自身绑定之后。在onPause()
中,这个例子总是检查用户是否激活了跟踪,如果没有,它首先停止服务。这允许服务在不需要跟踪的情况下终止,从而防止服务在没有实际工作要做的情况下永远挂在内存中。
运行这个例子,并按下 Start Tracking 按钮将会启动持久服务和LocationManager
。用户可以在这一点上离开应用,并且服务将保持运行,同时记录来自 GPS 的所有输入位置更新。当用户返回到这个应用时,他们可以看到服务仍然在运行,并且显示当前存储的位置点的数量。按 Stop Tracking 将结束该过程,并允许服务在用户再次离开活动时立即终止。
6–6 岁。启动其他应用
问题
您的应用需要特定的功能,而设备上的另一个应用已经对该功能进行了编程。为了避免重叠功能,您希望启动该作业的另一个应用。
解决方案
(API 一级)
使用一个隐含的意图来告诉系统你想做什么,并确定是否有任何应用可以满足需要。大多数情况下,开发人员以明确的方式使用意图来开始另一个活动或服务,就像这样:
Intent intent = new Intent(this, NewActivity.class); startActivity(intent);
通过声明我们想要启动的特定组件,其交付意图非常明确。我们也有能力根据意图的动作、类别、数据和类型来定义意图,以定义我们想要完成什么任务的更隐含的需求。
当以这种方式启动时,外部应用总是在与您的应用相同的 Android 任务中启动,因此一旦操作完成(或者如果用户退出),用户就会返回到您的应用。这保持了无缝的体验,从用户的角度来看,允许多个应用作为一个整体。
它是如何工作的
当以这种方式定义意图时,可能不清楚您必须包括什么信息,因为没有发布的标准,并且提供相同服务(例如,读取 PDF 文件)的两个应用可能定义稍微不同的过滤器来监听传入的意图。您希望确保并为系统(或用户)提供足够的信息,以选择处理所需任务的最佳应用。
定义几乎所有隐含意图的核心数据是动作;在构造函数中或通过Intent.setAction()
传递的字符串值。这个值告诉 Android 你想做什么,是查看一段内容,发送一条消息,选择一个选项,还是你有什么。由此,所提供的字段是特定于场景的,并且通常多种组合可以得到相同的结果。让我们来看看一些有用的例子。
阅读 PDF 文件
显示 PDF 文档的组件不包括在核心 SDK 中,尽管今天市场上几乎每个消费 Android 设备都附带了 PDF 阅读器应用,Android Market 上还有许多其他应用。因此,在应用中嵌入 PDF 显示功能可能没有意义。
相反,下面的清单 6–14 说明了如何找到并启动另一个应用来查看 PDF。
清单 6–14。 查看 PDF 的方法
private void viewPdf(Uri file) { Intent intent; intent = new Intent(Intent.ACTION_VIEW); intent.setDataAndType(file, "application/pdf"); try { startActivity(intent); } catch (ActivityNotFoundException e) { //No application to view, ask to download one AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle("No Application Found"); builder.setMessage("We could not find an application to view PDFs." +" Would you like to download one from Android Market?"); builder.setPositiveButton("Yes, Please", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent marketIntent = new Intent(Intent.ACTION_VIEW); marketIntent.setData(Uri.parse("market://details?id=com.adobe.reader")); startActivity(marketIntent); } }); builder.setNegativeButton("No, Thanks", null); builder.create().show(); } }
此示例方法将使用找到的最佳应用打开设备(内部或外部存储器)上的任何本地 PDF 文件。如果在设备上找不到查看 pdf 的应用,我们鼓励用户去 Android Market 下载一个。
我们为此创建的意图是使用通用的Intent.ACTION_VIEW
动作字符串构建的,告诉系统我们想要查看意图中提供的数据。数据文件本身及其 MIME 类型也被设置为告诉系统我们想要查看哪种数据。
提示: Intent.setData()
和Intent.setType()
使用时互相清零对方以前的值。如果您需要同时设置两者,请使用示例中的Intent.setDataAndType(),
。
如果startActivity()
因ActivityNotFoundException
而失败,这意味着用户的设备上没有安装可以查看 pdf 的应用。我们希望我们的用户有完整的体验,所以如果发生这种情况,我们会显示一个对话框告诉他们问题,并询问他们是否愿意去市场上买一个阅读器。如果用户按下 Yes,我们使用另一个隐含的意图来请求 Android Market 直接打开到 Adobe Reader 的应用页面,这是一个用户可以下载来查看 PDF 文件的免费应用。我们将在下一个秘籍中讨论用于这个目的的Uri
方案。
注意,示例方法将一个Uri
参数传递给本地文件。以下是如何检索位于内部存储上的文件的Uri
的示例:
String filename = NAME_OF YOUR_FILE; File internalFile = getFileStreamPath(filename); Uri internal = Uri.fromFile(internalFile);
方法getFileStreamPath()
是从Context
调用的,所以如果这个代码不在活动中,你必须引用一个Context
对象来调用。以下是如何为位于外部存储器上的文件创建一个Uri
:
String filename = NAME_OF YOUR_FILE; File externalFile = new File(Environment.getExternalStorageDirectory(), filename); Uri external = Uri.fromFile(externalFile);
这个例子也适用于任何其他文档类型,只需简单地改变附加到意图的 MIME 类型。
与朋友分享
开发人员在他们的应用中包含的另一个流行特性是与他人共享应用内容的方法;通过电子邮件、短信和著名的社交网络。所有的 Android 设备都包括电子邮件和短信应用,大多数希望通过社交网络(如脸书或 Twitter)分享的用户也在他们的设备上安装了这些移动应用。
事实证明,这项任务也可以使用隐式意图来完成,因为大多数应用都会以某种方式响应Intent.ACTION_SEND
动作字符串。 清单 6–15 是一个允许用户通过一个意向请求向任何媒体发帖的例子。
清单 6–15。 分享意向
private void shareContent(String update) { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_TEXT, update); startActivity(Intent.createChooser(intent, "Share...")); }
这里,我们告诉系统我们有一段文本要发送,作为额外的内容传入。这是一个非常普通的请求,我们希望不止一个应用能够处理它。默认情况下,Android 会给用户一个应用列表,让用户选择想要打开的应用。此外,一些设备为用户提供了一个复选框,将他们的选择设置为默认值,这样列表就不会再显示了!
我们希望对这个过程有更多一点的控制,因为我们也希望每次都有多个结果。因此,我们没有将意图直接传递给startActivity()
,而是首先通过Intent.createChooser()
传递,这允许我们定制标题,并保证选择列表将始终显示。
当用户选择一个选项时,特定的应用将启动,并在消息输入框中预填充EXTRA_TEXT
,准备共享!
6–7 岁。启动系统应用
问题
您的应用需要特定的功能,而设备上的某个系统应用已经对该功能进行了编程。为了避免重叠功能,您希望启动作业的系统应用。
解决方案
(API 一级)
使用隐含的意图告诉系统你对哪个应用感兴趣。每个系统应用订阅一个定制的Uri
方案,该方案可以作为数据插入到一个隐含的意图中,以表示您需要启动的特定应用。
以这种方式启动时,外部应用总是在与您的应用相同的任务中启动,因此一旦任务完成(或者如果用户退出),用户就会返回到您的应用。这保持了无缝的体验,从用户的角度来看,允许多个应用作为一个整体。
工作原理
下面所有的例子都将构造可以用来在不同状态下启动系统应用的意图。一旦构建完成,您应该通过将所述意图传递给startActivity().
来启动这些应用
浏览器
可以启动浏览器应用来显示网页或运行网络搜索。
要显示网页,请构建并启动以下意图:
Intent pageIntent = new Intent(); pageIntent.setAction(Intent.ACTION_VIEW); pageIntent.setData(Uri.parse(“http://WEB_ADDRESS_TO_VIEW”));
这将数据字段中的Uri
替换为您想要查看的页面。要在浏览器中启动 web 搜索,请构建并启动以下意图:
Intent searchIntent = new Intent(); searchIntent.setAction(Intent.ACTION_WEB_SEARCH); searchIntent.putExtra(SearchManager.QUERY, STRING_TO_SEARCH);
这将把您想要执行的搜索查询作为额外内容放在意图中。
电话拨号程序
可以启动拨号器应用,使用以下意图向特定号码发出呼叫:
Intent dialIntent = new Intent(); dialIntent.setAction(Intent.ACTION_DIAL); dialIntent.setData(Uri.Parse(“tel:8885551234”);
这将数据 Uri 中的电话号码替换为要呼叫的号码。
**注意:**这个动作只是调出拨号器;它实际上并不发出呼叫。Intent.ACTION_CALL
可以用来直接拨打电话,尽管谷歌不鼓励在大多数情况下使用它。使用ACTION_CALL
还需要在清单中声明android.permission.CALL_PHONE
权限。
地图
可以启动设备上的地图应用来显示位置或提供两点之间的方向。如果您知道要绘制地图的位置的纬度和经度,则创建以下意图:
Intent mapIntent = new Intent(); mapIntent.setAction(Intent.ACTION_VIEW); mapIntent.setData(Uri.parse(“geo:latitude,longitude”));
这将替换您所在位置的经纬度坐标。例如,Uri
"geo:37.422,122.084"
会标出谷歌总部的位置。如果您知道要显示的位置的地址,则创建以下意图:
Intent mapIntent = new Intent(); mapIntent.setAction(Intent.ACTION_VIEW); mapIntent.setData(Uri.parse(“geo:0,0?q=ADDRESS”));
这将插入您想要映射的地址。例如,Uri
"geo:0,0?q=1600 Amphitheatre Parkway, Mountain View, CA 94043"
会绘制谷歌总部的地址。
**提示:**地图应用也将接受一个Uri
,其中地址查询中的空格将被替换为“+”字符。如果对包含空格的字符串进行编码有困难,请尝试用“+”替换它们。
如果您想要显示至位置之间的方向,请创建以下意图:
Intent mapIntent = new Intent(); mapIntent.setAction(Intent.ACTION_VIEW); mapIntent.setData(Uri.parse(“http://maps.google.com/maps?saddr=lat,lng&daddr=lat,lng”));
这将插入起始和结束地址的位置。
如果您希望打开一个地址开放的地图应用,也可以只包含其中一个参数。例如,Uri
"http://maps.google.com/maps?&daddr=37.422,122.084"
将显示地图应用与目的地位置预填充,但允许用户输入自己的起始地址。
电子邮件
设备上的任何电子邮件应用都可以使用以下意图启动到撰写模式:
Intent mailIntent = new Intent(); mailIntent.setAction(Intent.ACTION_SEND); mailIntent.setType(“message/rfc822”); mailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {"recipient@gmail.com"}); mailIntent.putExtra(Intent.EXTRA_CC, new String[] {"carbon@gmail.com"}); mailIntent.putExtra(Intent.EXTRA_BCC, new String[] {"blind@gmail.com"}); mailIntent.putExtra(Intent.EXTRA_SUBJECT, "Email Subject"); mailIntent.putExtra(Intent.EXTRA_TEXT, "Body Text"); mailIntent.putExtra(Intent.EXTRA_STREAM, URI_TO_FILE);
在这种情况下,action 和 type 字段是显示空白电子邮件的唯一必需部分。所有剩余的 extras 都预先填充了电子邮件消息的特定字段。请注意,EXTRA_EMAIL
(填充 To:字段)、EXTRA_CC
和EXTRA_BCC
被传递给了字符串数组,即使那里只放置了一个收件人。也可以使用EXTRA_STREAM
在意向中指定文件附件。这里传递的值应该是一个指向要附加的本地文件的Uri
。
如果您需要在电子邮件中附加多个文件,要求会稍有变化,如下所示:
`Intent mailIntent = new Intent();
mailIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
mailIntent.setType(“message/rfc822”);
mailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] {"recipient@gmail.com"});
mailIntent.putExtra(Intent.EXTRA_CC, new String[] {"carbon@gmail.com"});
mailIntent.putExtra(Intent.EXTRA_BCC, new String[] {"blind@gmail.com"});
mailIntent.putExtra(Intent.EXTRA_SUBJECT, "Email Subject");
mailIntent.putExtra(Intent.EXTRA_TEXT, "Body Text");
ArrayList files = new ArrayList();
files.add(URI_TO_FIRST_FILE);
files.add(URI_TO_SECOND_FILE);
//...Repeat add() as often as necessary to add all the files you need
mailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, files);`
注意,意向的动作字符串现在是ACTION_SEND_MULTIPLE
。除了作为EXTRA_STREAM
添加的数据之外,所有的主字段保持不变。这个例子创建了一个指向您想要附加的文件的 uri 列表,并使用putParcelableArrayListExtra()
添加它们。
对于用户来说,在他们的设备上有多个应用可以处理这些内容并不少见,所以通常谨慎的做法是在将这些构建的意图传递给startActivity()
之前用Intent.createChooser()
包装它们。
短信(Messages)
可以使用以下意图将消息应用启动到新 SMS 消息的撰写模式:
Intent smsIntent = new Intent(); smsIntent.setAction(Intent.ACTION_VIEW); smsIntent.setType(“vnd.android-dir/mms-sms”); smsIntent.putExtra(“address”, “8885551234”); smsIntent.putExtra(“sms_body”, “Body Text”);
与撰写电子邮件一样,您必须至少设置操作和类型,以启动带有空白消息的应用。包括地址和 sms_body extras 允许应用预先填充消息的收件人(地址)和正文文本(sms_body)。
请注意,这两个键都没有在 Android 框架中定义的常量,这意味着它们将来会发生变化。然而,在撰写本文时,这些键在所有版本的 Android 上都表现正常。
联系提货人
应用可以启动默认联系人选取器,以便用户使用以下意图从他们的联系人数据库中进行选择:
Intent pickIntent = new Intent(); pickIntent.setAction(Intent.ACTION_PICK); pickIntent.setData(URI_TO_CONTACT_TABLE);
这个意图要求将您感兴趣的 Contacts 表的CONTENT_URI
传递到数据字段中。由于在 API Level 5 (Android 2.0)和更高版本中对 Contacts API 进行了重大更改,如果您支持跨边界的版本,这可能与Uri
不同。
例如,要在 2.0 之前的设备上从联系人列表中选择一个人,我们将传递
android.provider.Contacts.People.CONTENT_URI
但是,在 2.0 和更高版本中,类似的数据将通过传递
android.provider.ContactsContract.Contacts.CONTENT_URI
关于您需要访问的联系数据,请务必查阅 API 文档。
安卓市场
Android Market 可以从应用中启动,以显示特定应用的详细信息页面或运行特定关键字的搜索。要启动特定的应用市场页面,请使用以下意图:
Intent marketIntent = new Intent(); marketIntent.setAction(Intent.ACTION_VIEW); marketIntent.setData(Uri.parse(“market://details?id=PACKAGE_NAME_HERE”));
这将插入您要显示的应用的唯一包名(如“com.adobe.reader”)。如果您想通过搜索查询打开市场,请使用以下意图:
Intent marketIntent = new Intent(); marketIntent.setAction(Intent.ACTION_VIEW); marketIntent.setData(Uri.parse(“market://search?q=SEARCH_QUERY”));
插入要搜索的查询字符串。搜索查询本身可以采取三种主要形式之一:
q=<simple text string here>
q=pname:<package name here>
q=pub:<developer name here>
在这种情况下,将搜索开发人员姓名字段,只返回完全匹配的内容。
6–8 岁。让其他应用启动您的应用
问题
您已经创建了一个绝对擅长完成特定任务的应用,并且您希望为设备上的其他应用提供一个接口,以便能够运行您的应用。
解决方案
(API 一级)
为您想要公开的活动或服务创建一个IntentFilter
,然后公开记录正确访问它所需的动作、数据类型和附加内容。回想一下,意图的动作、类别和数据/类型都可以用作将请求匹配到您的应用的标准。任何额外的必需或可选参数都应该作为额外参数传入。
它是如何工作的
假设您已经创建了一个应用,其中包含一个播放视频的活动,并在播放过程中在屏幕顶部选择视频的标题。您希望允许其他应用使用您的应用播放视频,因此我们需要为应用定义一个有用的意图结构来传递所需的数据,然后在应用清单中的活动上创建一个IntentFilter
来匹配。
这个假设的活动需要两个数据来完成它的工作:
本地或远程视频的Uri
一个代表视频标题的String
如果应用专门处理某种类型的视频,我们可以定义一个通用的动作(比如 ACTION_VIEW ),并根据我们想要处理的视频内容的数据类型进行更具体的过滤。清单 6–16 是一个如何在清单中定义活动的例子,以这种方式过滤意图。
**清单 6–16。**androidmanifest . XML元素带数据类型过滤器
<activity android:name=".PlayerActivity"> <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="video/h264" /> </intent-filter> </activity>
该过滤器将匹配任何带有Uri
数据的意图,这些数据要么被明确声明为 H.264 视频剪辑,要么在检查Uri
文件时被确定为 H.264。然后,外部应用将能够调用此活动,使用以下代码行播放视频:
Uri videoFile = A_URI_OF_VIDEO_CONTENT; Intent playIntent = new Intent(Intent.ACTION_VIEW); playIntent.setDataAndType(videoFile, “video/h264”); playIntent.putExtra(Intent.EXTRA_TITLE, “My Video”); startActivity(playIntent);
在某些情况下,外部应用直接引用这个播放器作为目标可能更有用,而不管它们想要传入的视频类型。在这种情况下,我们将为意图实现创建一个唯一的自定义操作字符串。清单中附加到活动的过滤器只需要匹配定制的动作字符串。参见清单 6–17。
**清单 6–17。**Android manifest . XML元素带自定义动作过滤器
<activity android:name=".PlayerActivity"> <intent-filter> <action android:name="com.examples.myplayer.PLAY" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>
外部应用可以调用此活动,使用以下代码播放视频:
Uri videoFile = A_URI_OF_VIDEO_CONTENT; Intent playIntent = new Intent(“com.examples.myplayer.PLAY”); playIntent.setData(videoFile); playIntent.putExtra(Intent.EXTRA_TITLE, “My Video”); startActivity(playIntent);
处理成功的发射
不管意图如何与活动相匹配,一旦活动启动,我们希望检查活动完成其预期目的所需的两条数据的传入意图。参见清单 6–18。
清单 6–18。 活动考察意向
`public class PlayerActivity extends Activity {
public static final String ACTION_PLAY = "com.examples.myplayer.PLAY";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//Inspect the Intent that launched us
Intent incoming = getIntent();
//Get the video URI from the data field
Uri videoUri = incoming.getData();
//Get the optional title extra, if it exists
String title;
if(incoming.hasExtra(Intent.EXTRA_TITLE)) else {
title = "";
}
/* Begin playing the video and displaying the title */
}
/* Remainder of the Activity Code */
}`
活动发起时,可以通过Activity.getIntent()
检索调用意图。因为视频内容的Uri
是在 Intent 的数据字段中传递的,所以通过调用Intent.getData()
对其进行解包。我们已经确定视频的标题是调用意图的可选值,所以我们检查 extras 包,首先查看调用者是否决定传递它;如果存在,该值也会从意图中解包。
注意,本例中的PlayerActivity
的确将定制动作字符串定义为一个常量,但是在我们上面构建的启动活动的示例意图中没有引用它。因为这个调用来自外部应用,所以它不能访问这个应用中定义的共享公共常量。
因此,尽可能重用 SDK 中已经存在的 Intent extra 键也是一个好主意,而不是定义新的常量。在本例中,我们选择了标准意图。EXTRA_TITLE 来定义要传递的可选 EXTRA,而不是为该值创建一个自定义键。
6–9 岁。与联系人交互
问题
您的应用需要直接与 Android 向用户联系人公开的ContentProvider
进行交互,以添加、查看、更改或删除数据库中的信息。
解决方案
(API 等级 5)
使用ContactsContract
公开的接口访问数据。ContactsContract
是一个庞大的ContentProvider
API,它试图将存储在系统中的来自多个用户账户的联系信息聚合到一个数据存储中。结果是一个由Uris
、表和列组成的迷宫,从中可以访问和修改数据。
联系人结构是一个具有三层的层次结构:联系人、原始联系人和数据。
联系人在概念上代表一个人,是 Android 认为代表同一个人的所有RawContacts
的集合。
RawContacts
表示存储在设备中的来自特定设备帐户的数据集合,例如用户的电子邮件地址簿、脸书帐户或其他。
数据元素是附加到每个RawContacts
的特定信息,比如电子邮件地址、电话号码或邮政地址。
完整的 API 有太多的组合和选项,我们无法在这里一一介绍,所以请查阅 SDK 文档了解所有的可能性。我们将研究如何构建执行查询和更改 contacts 数据集的基本构建块。
它是如何工作的
Android Contacts API 归结为一个包含多个表和连接的复杂数据库。因此,访问数据的方法与从应用访问任何其他 SQLite 数据库的方法没有什么不同。
列出/查看联系人
让我们看一个示例活动,它列出了数据库中的所有联系人条目,当选择一个条目时,会显示更多的细节。参见清单 6–19。
**重要提示:**为了在应用中显示联系人 API 的信息,您需要在应用清单中声明android.permission.READ_CONTACTS
。
清单 6–19。 活动显示联系人
`public class ContactsActivity extends ListActivity implements AdapterView.OnItemClickListener {
Cursor mContacts;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Return all contacts, ordered by name
String[] projection = new String[] { ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME };
mContacts = managedQuery(ContactsContract.Contacts.CONTENT_URI,
projection, null, null, ContactsContract.Contacts.DISPLAY_NAME);
// Display all contacts in a ListView
SimpleCursorAdapter mAdapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_1, mContacts,
new String[] ,
newint[] );
setListAdapter(mAdapter);
// Listen for item selections
getListView().setOnItemClickListener(this);
}
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
if (mContacts.moveToPosition(position)) {
int selectedId = mContacts.getInt(0); // _ID column
// Gather email data from email table
Cursor email = getContentResolver().query(
CommonDataKinds.Email.CONTENT_URI,
new String[] ,
ContactsContract.Data.CONTACT_ID + " = " + selectedId, null, null);
// Gather phone data from phone table
Cursor phone = getContentResolver().query(
CommonDataKinds.Phone.CONTENT_URI,
new String[] ,
ContactsContract.Data.CONTACT_ID + " = " + selectedId, null, null);
// Gather addresses from address table
Cursor address = getContentResolver().query(
CommonDataKinds.StructuredPostal.CONTENT_URI,
new String[] ,
ContactsContract.Data.CONTACT_ID + " = " + selectedId, null, null);
//Build the dialog message
StringBuilder sb = new StringBuilder();
sb.append(email.getCount() + " Emails\n");
if (email.moveToFirst()) {
do {
sb.append("Email: " + email.getString(0));
sb.append('\n');
} while (email.moveToNext());
sb.append('\n');
}
sb.append(phone.getCount() + " Phone Numbers\n");
if (phone.moveToFirst()) {
do {
sb.append("Phone: " + phone.getString(0));
sb.append('\n');
} while (phone.moveToNext());
sb.append('\n');
}
sb.append(address.getCount() + " Addresses\n");
if (address.moveToFirst()) {
do {
sb.append("Address:\n" + address.getString(0));
} while (address.moveToNext());
sb.append('\n');
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(mContacts.getString(1)); // Display name
builder.setMessage(sb.toString());
builder.setPositiveButton("OK", null);
builder.create().show();
// Finish temporary cursors
email.close();
phone.close();
address.close();
}
}
}`
正如您所看到的,在这个 API 中引用所有的表和列会导致非常冗长的代码。本例中对Uris
、表和列的所有引用都是源于ContactsContract
的内部类。在与 Contacts API 交互时,验证您引用的是正确的类是很重要的,因为任何不是源于ContactsContract
的 Contacts 类都是不推荐的和不兼容的。
创建活动后,我们通过用Contacts.CONTENT_URI
调用Activity.managedQuery()
对核心 Contacts 表进行简单的查询,只请求我们需要将光标放在ListAdapter
中的列。结果光标显示在用户界面上的列表中。这个例子利用了ListActivity
的便利行为来提供一个ListView
作为内容视图,这样我们就不必管理这些组件了。
此时,用户可以滚动设备上的所有联系人条目,并点击其中一个条目以获得更多信息。当一个列表项被选中时,该特定联系人的 _ID 值被记录下来,应用转到其他的ContactsContract.Data
表来收集更详细的信息。请注意,这个联系人的数据分布在多个表中(电子邮件表中的电子邮件、电话表中的电话号码等等),需要多次查询才能获得。
每个CommonDataKinds
表都有一个惟一的CONTENT_URI
供查询引用,还有一组惟一的列别名用于请求数据。这些数据表中的所有行都通过Data.CONTACT_ID
链接到特定的联系人,因此每个游标都要求只返回值匹配的行。
收集了所选联系人的所有数据后,我们遍历结果并在对话框中显示给用户。由于这些表中的数据是多个来源的集合,因此所有这些查询返回多个结果的情况并不少见。对于每个光标,我们显示结果的数量,然后追加每个包含的值。当所有的数据组成后,对话框被创建并显示给用户。
最后一步,所有临时和非托管游标在不再需要时立即关闭。
Running the Application
在设置了任意数量帐户的设备上运行该应用时,您可能会注意到的第一件事是,该列表似乎非常长,肯定比运行与设备捆绑的联系人应用时显示的要长得多。联系人 API 允许存储分组条目,这些条目可能对用户隐藏并用于内部目的。Gmail 经常使用它来存储收到的电子邮件地址,以便快速访问,即使该地址与真实的联系人无关。
在下一个例子中,我们将展示如何过滤这个列表,但是现在我们要惊叹于联系人表中真正存储的数据量。
更改/添加联系人
现在让我们看一个操作特定联系人数据的示例活动。参见清单 6–20。
**重要提示:**为了与应用中的联系人 API 进行交互,您必须在应用清单中声明android.permission.READ_CONTACTS
和android.permission.WRITE_CONTACTS
。
清单 6–20。 活动写入联系人 API
`public class ContactsEditActivity extends ListActivity implements
AdapterView.OnItemClickListener, DialogInterface.OnClickListener {
private static final String TEST_EMAIL = "test@email.com";
private Cursor mContacts, mEmail;
private int selectedContactId;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Return all contacts, ordered by name
String[] projection = new String[] { ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME };
//List only contacts visible to the user
mContacts = managedQuery(ContactsContract.Contacts.CONTENT_URI,
projection,
ContactsContract.Contacts.IN_VISIBLE_GROUP+" = 1",
null, ContactsContract.Contacts.DISPLAY_NAME);
// Display all contacts in a ListView
SimpleCursorAdapter mAdapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_1, mContacts,
new String[] ,
newint[] );
setListAdapter(mAdapter);
// Listen for item selections
getListView().setOnItemClickListener(this);
}
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
if (mContacts.moveToPosition(position)) {
selectedContactId = mContacts.getInt(0); // _ID column
// Gather email data from email table
String[] projection = new String[] { ContactsContract.Data._ID,
ContactsContract.CommonDataKinds.Email.DATA };
mEmail = getContentResolver().query(
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
projection,
ContactsContract.Data.CONTACT_ID+" = "+selectedContactId, null, null);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Email Addresses");
builder.setCursor(mEmail, this, ContactsContract.CommonDataKinds.Email.DATA);
builder.setPositiveButton("Add", this);
builder.setNegativeButton("Cancel", null);
builder.create().show();
}
}
@Override
public void onClick(DialogInterface dialog, int which) {
//Data must be associated with a RAW contact, retrieve the first raw ID
Cursor raw = getContentResolver().query(
ContactsContract.RawContacts.CONTENT_URI,
new String[] ,
ContactsContract.Data.CONTACT_ID+" = "+selectedContactId, null, null);
if(!raw.moveToFirst()) {
return;
}
int rawContactId = raw.getInt(0);
ContentValues values = new ContentValues();
switch(which) {
case DialogInterface.BUTTON_POSITIVE:
//User wants to add a new email
values.put(ContactsContract.CommonDataKinds.Email.RAW_CONTACT_ID, rawContactId);
values.put(ContactsContract.Data.MIMETYPE,
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE);
values.put(ContactsContract.CommonDataKinds.Email.DATA, TEST_EMAIL);
values.put(ContactsContract.CommonDataKinds.Email.TYPE,
ContactsContract.CommonDataKinds.Email.TYPE_OTHER);
getContentResolver().insert(ContactsContract.Data.CONTENT_URI, values);
break;
default:
//User wants to edit selection
values.put(ContactsContract.CommonDataKinds.Email.DATA, TEST_EMAIL);
values.put(ContactsContract.CommonDataKinds.Email.TYPE,
ContactsContract.CommonDataKinds.Email.TYPE_OTHER);
getContentResolver().update(ContactsContract.Data.CONTENT_URI, values,
ContactsContract.Data._ID+" = "+mEmail.getInt(0), null);
break;
}
//Don't need the email cursor anymore
mEmail.close();
}
}`
在这个例子中,我们像以前一样开始,对 Contacts 数据库中的所有条目执行查询。这一次,我们提供了单一的选择标准:
ContactsContract.Contacts.IN_VISIBLE_GROUP+" = 1"
这一行的作用是将返回的条目限制为只包括那些通过联系人用户界面对用户可见的条目。这将(在某些情况下,极大地)减小活动中显示的列表的大小,并使其与联系人应用中显示的列表更加匹配。
当用户从该列表中选择一个联系人时,将显示一个对话框,其中列出了该联系人的所有电子邮件条目。如果从列表中选择了特定的地址,则编辑该条目;并且如果按下添加按钮,则添加新的电子邮件地址条目。为了简化示例,我们不提供输入新电子邮件地址的界面。而是插入一个常数值,作为新记录或对所选记录的更新。
电子邮件地址等数据元素只能与一个RawContact
相关联。因此,当我们想要添加一个新的电子邮件地址时,我们必须获得由用户选择的更高级别联系人表示的 RawContacts 之一的 ID。出于示例的目的,我们对哪一个不太感兴趣,所以我们检索第一个匹配的 RawContact 的 ID。只有在执行插入时才需要该值,因为更新引用了表中已经存在的电子邮件记录的不同行 ID。
还要注意的是,CommonDataKinds
中提供的用于读取该数据的别名Uri
不能用于进行更新和更改。插入和更新必须直接在ContactsContract.DataUri
上调用。这意味着(除了在操作方法中引用不同的Uri
之外)还必须指定一个额外的元数据MIMETYPE
。如果没有为插入的数据设置MIMETYPE
字段,后续查询可能不会将其识别为联系人的电子邮件地址。
Aggregation at Work
因为这个示例通过添加或编辑具有相同值的电子邮件地址来更新记录,所以它提供了一个独特的机会来实时查看 Android 的聚合操作。当您运行这个示例应用时,您可能会注意到这样一个事实,即添加或编辑联系人以给他们相同的电子邮件地址经常会触发 Android 开始认为以前分开的联系人现在是同一个人。即使在这个示例应用中,当附加到核心 Contacts 表的托管查询更新时,请注意,某些联系人会随着它们聚合在一起而消失。
**注意:**Android 模拟器上没有完全实现联系人聚合行为。要完全看到这种效果,您需要在真实设备上运行代码。
维护参考
Android Contacts API 引入了另一个重要的概念,这取决于应用的范围。由于这种聚合过程的发生,引用联系人的不同行 ID 变得非常不稳定;当某个联系人与另一个联系人聚合在一起时,该联系人可以接收新的 ID。
如果您的应用需要对特定联系人的长期引用,建议您的应用保留ContactsContract.Contacts.LOOKUP_KEY
,而不是行 ID。当使用该键查询联系人时,还会提供一个特殊的Uri
作为ContactsContract.Contacts.CONTENT_LOOKUP_URI
。使用这些值来长期查询记录将保护您的应用不会被自动聚合过程所混淆。
6 到 10 岁。挑选设备媒体
问题
您的应用需要导入用户选择的媒体项目(音频、视频或图像)以供显示或回放。
解决方案
(API 一级)
使用以Intent.ACTION_GET_CONTENT
为目标的隐含意图,调出系统媒体选择器界面。用感兴趣的媒体(音频、视频或图像)的匹配内容类型激发这个意图,将为用户提供一个选择器界面来选择一个项目,并且意图结果将包括一个指向他们所做选择的 Uri。
它是如何工作的
让我们看看在一个示例活动的上下文中使用的这种技术。参见清单 6–21 和清单 6–22。
清单 6–21。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <Button android:id="@+id/imageButton" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Images" /> <Button android:id="@+id/videoButton" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Video" />
<Button android:id="@+id/audioButton" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Audio" /> </LinearLayout>
清单 6–22。 活动挑选媒体
`public class MediaActivity extends Activity implements View.OnClickListener {
private static final intREQUEST_AUDIO = 1;
private static final intREQUEST_VIDEO = 2;
private static final intREQUEST_IMAGE = 3;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button images = (Button)findViewById(R.id.imageButton);
images.setOnClickListener(this);
Button videos = (Button)findViewById(R.id.videoButton);
videos.setOnClickListener(this);
Button audio = (Button)findViewById(R.id.audioButton);
audio.setOnClickListener(this);
}
@Override
protectedvoid onActivityResult(int requestCode, int resultCode, Intent data) {
if(resultCode == Activity.RESULT_OK) {
//Uri to user selection returned in the Intent
Uri selectedContent = data.getData();
if(requestCode == REQUEST_IMAGE) {
//Display the image
}
if(requestCode == REQUEST_VIDEO) {
//Play the video clip
}
if(requestCode == REQUEST_AUDIO) {
//Play the audio clip
}
}
}
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.setAction(Intent.ACTION_GET_CONTENT);
switch(v.getId()) {
case R.id.imageButton:
intent.setType("image/");
startActivityForResult(intent, REQUEST_IMAGE);
return;
case R.id.videoButton:
intent.setType("video/ ");
startActivityForResult(intent, REQUEST_VIDEO);
return;
case R.id.audioButton:
intent.setType("audio/*");
startActivityForResult(intent, REQUEST_AUDIO);
return;
default:
return;
}
}
}`
这个例子有三个按钮供用户按下,每个按钮针对一种特定类型的媒体。当用户按下这些按钮中的任何一个时,带有Intent.ACTION_GET_CONTENT
动作字符串的意图被发送给系统,启动适当的选取器活动。如果用户选择了一个有效的条目,指向该条目的内容Uri
将在结果意图中返回,状态为RESULT_OK
。如果用户取消或退出选取器,状态将为RESULT_CANCELED
,并且意向的数据字段将为空。
随着媒体的Uri
被接收,应用现在可以自由地播放或显示被认为合适的内容。像MediaPlayer
和VideoView
这样的类将直接获取一个 Uri 来播放媒体内容,而Uri.getPath()
方法将返回一个可以传递给BitmapFactory.decodeFile()
的图像的文件路径。
6 至 11 日。保存到媒体商店
问题
您的应用希望存储媒体并将其插入设备的全局媒体存储中,以便所有应用都可以看到它。
解决方案
(API 一级)
利用 MediaStore 公开的 ContentProvider 接口来执行插入。除了媒体内容本身,该界面还允许您插入元数据来标记每个项目,例如标题、描述或创建时间。ContentProvider 插入操作的结果是一个 Uri,应用可以将它用作新媒体的目的地。
它是如何工作的
让我们来看一个将图像或视频剪辑插入 MediaStore 的例子。参见清单 6–23 和清单 6–24。
清单 6–23。 res/layout/main.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="fill_parent"> <Button android:id="@+id/imageButton" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Images" /> <Button android:id="@+id/videoButton" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="Video" /> </LinearLayout>
清单 6–24。 在 MediaStore 中保存数据的活动
`public class StoreActivity extends Activity implements View.OnClickListener {
private static final intREQUEST_CAPTURE = 100;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button images = (Button)findViewById(R.id.imageButton);
images.setOnClickListener(this);
Button videos = (Button)findViewById(R.id.videoButton);
videos.setOnClickListener(this);
}
@Override
protectedvoid onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode == REQUEST_CAPTURE&& resultCode == Activity.RESULT_OK) {
Toast.makeText(this, "All Done!", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onClick(View v) {
ContentValues values;
Intent intent;
Uri storeLocation;
switch(v.getId()) {
case R.id.imageButton:
//Create any metadata for image
values = new ContentValues(2);
values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, System.currentTimeMillis());
values.put(MediaStore.Images.ImageColumns.DESCRIPTION, "Sample Image");
//Insert metadata and retrieve Uri location for file
storeLocation = getContentResolver().insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
//Start capture with new location as destination
intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, storeLocation);
startActivityForResult(intent, REQUEST_CAPTURE);
return;
case R.id.videoButton:
//Create any metadata for video
values = new ContentValues(2);
values.put(MediaStore.Video.VideoColumns.ARTIST, "Yours Truly");
values.put(MediaStore.Video.VideoColumns.DESCRIPTION, "Sample Video Clip");
//Insert metadata and retrieve Uri location for file
storeLocation = getContentResolver().insert(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
//Start capture with new location as destination
intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, storeLocation);
startActivityForResult(intent, REQUEST_CAPTURE);
return;
default:
return;
}
}
}`
**注意:**由于这个例子与相机硬件交互,您应该在真实设备上运行它以获得完整的效果。事实上,在运行 Android 2.2 或更高版本的模拟器中有一个已知的错误,如果摄像机被访问,它将导致该示例崩溃。早期的仿真器会适当地执行代码,但是如果没有真正的硬件,这个例子就不那么有趣了。
在这个例子中,当用户点击任一按钮时,将与媒体本身相关联的元数据被插入到ContentValues
实例中。图像和视频共有的一些更常见的元数据列有:
TITLE
:内容标题的字符串值
DESCRIPTION
:内容描述的字符串值
DATE_TAKEN
:描述媒体捕获日期的整数值。用System.currentTimeMillis()
填充该字段,表示“现在”的时间
然后使用适当的CONTENT_URI
引用将ContentValues
插入媒体存储。请注意,元数据是在实际采集媒体本身之前插入的。成功插入的返回值是一个完全限定的 Uri,应用可以将它用作媒体内容的目的地。
在前面的例子中,我们使用了第四章中的简化方法,通过请求系统应用处理这个过程来捕获音频和视频。回想一下第四章中的内容,音频和视频捕获意图都可以通过传递,并额外声明结果的目的地。这是我们传递从 insert 返回的 Uri 的地方。
从捕获活动成功返回后,应用就不再需要做什么了。外部应用已将捕获的图像或视频保存到 MediaStore 插页引用的位置。这些数据现在对所有应用都可见,包括系统的图库应用。
总结
在这一章中,你学习了你的应用如何与 Android 操作系统直接交互。我们讨论了将操作置于背景中不同时间长度的几种方法。您了解了应用如何分担责任,互相启动以最好地完成手头的任务。最后,我们展示了系统如何公开其核心应用套件收集的内容供您的应用使用。在下一章,也是最后一章,我们将探讨如何利用大量公开可用的 Java 库来进一步增强您的应用。
七、使用库
聪明的 Android 开发者通过利用库来更快地将他们的应用交付给市场,库通过提供先前创建和测试的代码来减少开发时间。开发人员可以创建和使用自己的库,也可以使用他人创建的库,或者两者兼而有之。
本章的初始秘籍向你介绍创建和使用你自己的库。随后的菜谱向您介绍了 Kidroid 的 skiChart 图表库,用于呈现条形图和折线图,以及 IBM 的 MQTT 库,用于在您的应用中实现轻量级推送消息。
**提示:**OpenIntents.org 发布了一个来自不同厂商的库列表,你可能会发现它对你的应用开发有所帮助([www.openintents.org/en/libraries](http://www.openintents.org/en/libraries)
)。
7–1。创建 Java 库 jar
问题
您希望创建一个库,存储与 Android 无关的代码,并且可以在您的 Android 和非 Android 项目中使用。
解决办法
创建一个基于 JAR 的库,通过 JDK 命令行工具或 Eclipse 只访问 Java 5(和更早版本)API。
它是如何工作的
假设您计划创建一个简单的面向数学的工具库。这个库将由一个带有各种static
方法的MathUtils
类组成。清单 7–1 展示了这个类的早期版本。
清单 7–1。 MathUtils
通过static
方法 实现面向数学的工具
`// MathUtils.java
package com.apress.mathutils;
public class MathUtils
{
public static long factorial(long n)
{
if (n ⇐ 0)
return 1;
else
return n*factorial(n-1);
}
}`
MathUtils
目前由一个用于计算和返回阶乘的static factorial()
方法组成(可能用于计算排列和组合)。您可能最终会扩展这个类来支持快速傅立叶变换和其他不受java.lang.Math
类支持的数学运算。
**注意:**当创建一个存储 Android 无关代码的库时,确保只访问 Android 支持的标准 Java API(如 collections 框架)——不要访问不支持的 Java API(如 Swing)或特定于 Android 的 API(如 Android widgets)。另外,不要访问任何比 Java 版本 5 更新的标准 Java APIs。
用 JDK 创造数学
用 JDK 开发一个基于 JAR 的库是很简单的。执行以下步骤创建一个包含MathUtils
类的mathutils.jar
文件:
在当前目录中,创建一个包目录结构,由一个包含apress
子目录的com
子目录和一个包含mathutils
子目录的apress
子目录组成。
将清单 7–1 的MathUtils.java
源代码复制到存储在mathutils
中的MathUtils.java
文件中。
假设当前目录包含com
子目录,执行javac com/apress/mathutils/MathUtils.java
编译MathUtils.java
。一个MathUtils.class
文件存储在com/apress/mathutils
中。
通过执行jar cf mathutils.jar com/apress/mathutils/*.class
创建mathutils.jar
。产生的mathutils.jar
文件包含一个com/apress/mathutils/MathUtils.class
条目。
使用 Eclipse 创建数学工具
用 Eclipse 开发一个基于 JAR 的库有点复杂。执行以下步骤创建一个包含MathUtils
类的mathutils.jar
文件:
假设您已经安装了在第一章中讨论的 Eclipse 版本,如果还没有运行的话,启动这个 IDE。
从“文件”菜单中选择“新建”,从出现的弹出菜单中选择“Java 项目”。
在出现的 New Java Project 对话框中,将 mathutils
输入到项目名称文本字段中,然后单击 Finish 按钮。
展开包资源管理器的 mathutils 节点。
右键单击 src 节点(在 mathutils 下面),并选择“新建”,然后从出现的弹出菜单中选择“包”。
在出现的 New Java Package 对话框中,在 Name 字段中输入 com.apress.mathutils
并点击 Finish。
右键单击生成的 com.apress.mathutils 节点,选择“新建”,然后在生成的弹出菜单中选择“类”。
在出现的 New Java Class 对话框中,在 Name 字段中输入 MathUtils
并点击 Finish。
用清单 7–1 中的替换生成的 MathUtils.java 编辑器窗口中的框架内容。
右键单击 mathutils 项目节点,并从出现的弹出菜单中选择“构建项目”。(您可能必须先从“项目”菜单中取消选择“自动构建”。)
右键单击 mathutils 项目节点,并从出现的弹出菜单中选择“导出”。
在出现的 Export 对话框中,选择 Java 节点下的 JAR 文件并点击 Next 按钮。
在生成的 JAR 导出窗格中,保留默认值,但在 JAR 文件文本字段中输入mathutils.jar
。单击完成。产生的mathutils.jar
文件创建在 Eclipse 工作区的根目录中。
7–2。使用 Java 库 jar
问题
您已经成功构建了mathutils.jar
,并且想要学习如何将这个 JAR 文件集成到您的基于 Eclipse 的 Android 项目中。
解决办法
您将创建一个带有libs
目录的基于 Eclipse 的 Android 项目,并将mathutils.jar
复制到这个目录中。
**注意:**通常的做法是将库(.jar
文件和 Linux 共享对象库,.so
文件)存储在 Android 项目目录的libs
子目录中。Android build 系统自动获取在libs
中找到的文件,并将它们集成到 apk 中。如果这个库是一个共享对象库,它被存储在一个以lib
(不是libs
)开始的.apk
文件中。
它是如何工作的
现在你已经创建了mathutils.jar
,你需要一个 Android 应用来测试这个库。清单 7–2 将源代码呈现给一个UseMathUtils
基于单个活动的应用,该应用计算 5 阶乘,活动随后输出该阶乘。
清单 7–2。 UseMathUtils
调用 MathUtil``factorial()
方法计算 5 阶乘
`// UseMathUtils.java
package com.apress.usemathutils;
import android.app.Activity;
import android.os.Bundle;
import android.widget.TextView;
import com.apress.mathutils.MathUtils;
public class UseMathUtils extends Activity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
TextView tv = new TextView(this);
tv.setText("5! = "+MathUtils.factorial(5));
setContentView(tv);
}
}`
假设 Eclipse 正在运行,完成以下步骤来创建一个UseMathUtils
项目:
从“文件”菜单中选择“新建”,并从出现的弹出菜单中选择“项目”。
在 New Project 对话框中,展开向导树中的 Android 节点,选择该节点下的 Android 项目分支,点击 Next 按钮。
在出现的新 Android 项目 对话框中,在项目名称文本字段中输入 UseMathUtils
。输入的名称标识了存储UseMathUtils
项目的文件夹/目录。
如果“在工作区中创建新项目”单选按钮尚未选中,请选中它。
在构建目标下,选中要用作UseMathUtils
构建目标的适当 Android 目标的复选框。这个目标指定了您希望您的应用在哪个 Android 平台上构建。假设您只安装了 Android 2.3 平台,那么只有这个构建目标应该出现,并且应该已经被选中。
在属性下,在应用名称文本字段中输入 Use MathUtils
。这个人类可读的标题将出现在 Android 设备上。继续,在包名文本字段中输入 com.apress.usemathutils
。该值是包名称空间(遵循与 Java 编程语言中的包相同的规则),所有源代码都将驻留在该名称空间中。如果未选中创建活动复选框,请选中它,并在此复选框旁边的文本字段中输入 UseMathUtils
作为应用的开始活动的名称。未选中此复选框时,文本字段被禁用。最后,在 Min SDK Version 文本字段中输入整数 9
,以确定在 Android 2.3 平台上正确运行UseMathUtils
所需的最低 API 级别。
单击完成。
Eclipse 在 Package Explorer 窗口中创建一个UseMathUtils
节点。完成以下步骤来设置所有文件:
展开 UseMathUtils 节点,然后展开src
节点,再展开com.apress.usemathutils
节点。
双击 UseMathUtils.java 节点(在 com.apress.usemathutils 下面)并用清单 7–2 替换结果窗口中的框架内容。
右键单击 UseMathUtils 节点,在弹出的菜单中选择 New,然后选择 Folder。在出现的新文件夹 对话框中,将libs
输入到文件夹名称文本框中,并点击完成按钮。
使用您平台的文件管理器程序(如 Windows XP 的 Windows 资源管理器)选择先前创建的mathutils.jar
文件并将其拖到 libs 节点。如果出现文件操作 对话框,保持选择复制文件单选按钮并点击确定按钮。
右键单击 mathutils.jar 并在弹出菜单中选择“构建路径”,然后选择“配置构建路径”。
在出现的 UseMathUtils 的属性对话框中,选择 Libraries 选项卡并单击 Add Jars 按钮。
在出现的 JAR 选择 对话框中,展开 UseMathUtils 节点,然后展开 libs 节点。选择 mathutils.jar,点击 OK 关闭 JAR 选择 。第二次点击确定关闭 UseMathUtils 的属性。
您现在已经准备好运行这个项目了。从菜单栏中选择运行,然后从下拉菜单中选择运行。如果出现运行方式 对话框,选择 Android 应用并点击确定。Eclipse 启动模拟器,安装该项目的 APK,并运行应用,其输出显示在图 7–1 中。
图 7–1。 UseMathUtils
的简单用户界面可以扩展到让用户输入任意数字。
**注意:**检查这个应用的UseMathUtils.apk
文件(jar tvf UseMathUtils.apk
,你不会找到一个mathutils.jar
条目。相反,您会发现classes.dex
,它包含应用的 Dalvik 可执行字节码。classes.dex
还包含了MathUtils
classfile 的 Dalvik 等价物,因为 Android 构建系统解包 JAR 文件,用dx
工具处理它们的内容,将它们的 Java 字节码转换成 Dalvik 字节码,并将等价的 Dalvik 代码合并到classes.dex
。
7–3。创建 Android 库项目
问题
您希望创建一个库来存储 Android 特定的代码,比如定制的小部件或有或没有资源的活动。
解决办法
Android 2.2 和后续版本允许您创建 Android 库项目 ,这些项目是 Eclipse 项目,描述包含 Android 特定代码甚至资源的库。
它是如何工作的
假设您想要创建一个库,其中包含一个可重用的定制小部件,描述一个游戏棋盘(用于下棋、跳棋,甚至是井字游戏)。清单 7–3 揭示了这个库的GameBoard
类。
清单 7–3。 GameBoard
描述一个可重复使用的自定义控件,用于绘制不同的游戏棋盘
`// GameBoard.java
package com.apress.gameboard;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.view.View;
public class GameBoard extends View
{
private int nSquares, colorA, colorB;
private Paint paint;
private int squareDim;
public GameBoard(Context context, int nSquares, int colorA, int colorB)
{
super(context);
this.nSquares = nSquares;
this.colorA = colorA;
this.colorB = colorB;
paint = new Paint();
}
@Override
protected void onDraw(Canvas canvas)
{
for (int row = 0; row < nSquares; row++)
{
paint.setColor(((row & 1) == 0) ? colorA : colorB);
for (int col = 0; col < nSquares; col++)
{
int a = colsquareDim;
int b = row squareDim;
canvas.drawRect(a, b, a+squareDim, b+squareDim, paint);
paint.setColor((paint.getColor() == colorA) ? colorB : colorA);
}
}
}
@Override
protected void onMeasure(int widthMeasuredSpec, int heightMeasuredSpec)
{
// keep the view squared
int width = MeasureSpec.getSize(widthMeasuredSpec);
int height = MeasureSpec.getSize(heightMeasuredSpec);
int d = (width == 0) ? height : (height == 0) ? width :
(width < height) ? width : height;
setMeasuredDimension(d, d);
squareDim = width/nSquares;
}
}`
Android 定制小部件基于子类android.view.View
或其一个子类(如android.widget.TextView
)的视图。GameBoard
直接子类化View
,因为它不需要任何子类功能。
GameBoard
提供了几个字段,包括如下:
nSquares
存储游戏棋盘每边的方块数。典型值包括 3(3x 3 板)和 8(8x 8 板)。
colorA
存储偶数行上偶数方块的颜色,奇数行上奇数方块的颜色——行列编号从 0 开始。
colorB
存储偶数行奇数方块的颜色,奇数行偶数方块的颜色。
paint
存储对android.graphics.Paint
对象的引用,该对象用于在绘制游戏板时指定方块颜色(colorA
或colorB
)。
squareDim
存储正方形的尺寸——每边的像素数。
GameBoard
的构造函数通过在同名字段中存储其nSquares
、colorA
和colorB
参数来初始化这个小部件,并且还实例化了Paint
类。然而,在这样做之前,它将其context
参数传递给其View
超类。
注意: V iew
子类需要将一个android.content.Context
实例传递给它们的View
超类。这样做可以识别定制小部件运行的上下文(例如,一个活动)。定制小部件子类可以随后调用View
的Context getContext()
方法来返回这个Context
对象,这样它们就可以调用Context
方法来访问当前主题、资源等等。
Android 通过调用小部件的覆盖方法protected void onDraw(Canvas canvas)
来告诉定制小部件绘制自己。GameBoard
的onDraw(Canvas)
方法通过调用android.graphics.Canvas
的void drawRect(float left, float top, float right, float bottom, Paint paint)
方法来响应,为每个行/列交叉点绘制每个方块。最后一个paint
参数决定了那个方块的颜色。
Android 在调用onDraw(Canvas)
之前,必须对 widget 进行测量。它通过调用小部件的 overriding protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
方法来完成这个任务,其中传递的参数指定了父视图强加的水平和垂直空间需求。小部件通常将这些参数传递给View.MeasureSpec
嵌套类的static int getSize(int measureSpec)
方法,根据传递的measureSpec
参数返回小部件的精确宽度或高度。然后,必须将返回值或这些值的修改版本传递给View
的void setMeasuredDimension(int measuredWidth, int measuredHeight)
方法,以存储测得的宽度和高度。调用此方法失败会导致在运行时引发异常。因为游戏板应该是正方形的,GameBoard
的onMeasure(int, int)
方法将宽度和高度的最小值传递给setMeasuredDimension(int, int)
以确保游戏板是正方形的。
现在您已经知道了GameBoard
是如何工作的,您已经准备好创建一个存储这个类的库了。您将通过创建一个 Android 库项目来创建这个库。这样一个项目的好处是它是一个标准的 Android 项目,所以你可以像创建一个新的 app 项目一样创建一个新的 Android 库项目。
完成以下步骤来创建GameBoard
项目:
从“文件”菜单中选择“新建”,并从出现的弹出菜单中选择“项目”。
在 New Project 对话框中,展开向导树中的 Android 节点,选择该节点下的 Android 项目分支,点击 Next 按钮。
在出现的新 Android 项目 对话框中,在项目名称文本字段中输入 GameBoard
。输入的名称标识了存储GameBoard
项目的文件夹。
如果“在工作区中创建新项目”单选按钮尚未选中,请选中它。
在构建目标下,选中要用作GameBoard
构建目标的适当 Android 目标的复选框。这个目标指定了您希望您的应用在哪个 Android 平台上构建。假设您只安装了 Android 2.3 平台,那么只有这个构建目标应该会出现,并且它应该已经被选中了。
在“属性”下,将应用名称文本字段留空——该库不是一个应用,因此没有必要在此字段中输入值。继续,在包名文本字段中输入 com.apress.gameboard
。该值是包名称空间(遵循与 Java 编程语言中的包相同的规则),所有库源代码都将驻留在该名称空间中。如果选中了创建活动复选框,则取消选中它。未选中此复选框时,文本字段被禁用。最后,在 Min SDK 版本文本字段中输入整数 9
,以确定在 Android 2.3 平台上正确运行GameBoard
所需的最低 API 级别。
单击完成。
尽管您创建 Android 库项目的方式与创建常规应用项目的方式相同,但您必须调整GameBoard
的一些项目属性,以表明它是一个库项目:
在包浏览器中,右键单击 GameBoard 并从弹出菜单中选择 Properties。
在出现的游戏板属性对话框中,选择 Android 属性组并选中是库复选框。
单击应用按钮,然后单击确定。
新的GameBoard
项目现在被标记为 Android 库项目。然而,它还不包含包含清单 7–3 内容的GameBoard.java
源文件。在包浏览器的 game board/src/com/a press/game board 节点下创建这个源文件。
如果您愿意,您可以构建这个库(例如,右键单击 GameBoard 节点并从弹出菜单中选择 Build Project)。然而,没有必要这样做。当您生成使用此库的项目时,将自动生成该项目。你将在下一个秘籍中学习如何做这件事。
**注意:**如果你构建了GameBoard
库,你会发现一个com/apress/gameboard
目录结构,其中gameboard
包含GameBoard.class
和几个面向资源的类文件(即使GameBoard.java
不引用资源)。这就是基于 Android 库项目的库的本质。
7–4。使用 Android 库项目
问题
您已经成功构建了GameBoard
库,并且想要学习如何将这个库集成到您的基于 Eclipse 的 Android 项目中。
解决办法
在正在构建的 app 项目的属性中标识出要 Eclipse 的GameBoard
库,并构建 app。
它是如何工作的
现在你已经创建了GameBoard
,你需要一个 Android 应用来测试这个库。清单 7–4 将源代码呈现给一个UseGameBoard
基于单个活动的应用,该应用实例化这个库的GameBoard
类,并将其放置在活动的视图层次结构中。
清单 7–4。 UseGameBoard
将GameBoard
小部件放入活动的视图层次
`// UseGameBoard.java
package com.apress.usegameboard;
import android.app.Activity;
import android.graphics.Color;
import android.os.Bundle;
import com.apress.gameboard.GameBoard;
public class UseGameBoard extends Activity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
GameBoard gb = new GameBoard(this, 8, Color.BLUE, Color.WHITE);
setContentView(gb);
}
}`
假设 Eclipse 正在运行,完成以下步骤来创建一个UseGameBoard
项目:
从“文件”菜单中选择“新建”,并从出现的弹出菜单中选择“项目”。
在新建项目 对话框中,展开向导树中的 Android 节点,选择该节点下的 Android 项目分支,点击下一步按钮。
在出现的新 Android 项目 对话框中,在项目名称文本字段中输入 UseGameBoard
。输入的名称标识了存储UseGameBoard
项目的文件夹。
如果“在工作区中创建新项目”单选按钮尚未选中,请选中它。
在构建目标下,选中要用作UseGameBoard
构建目标的适当 Android 目标的复选框。这个目标指定了你希望你的应用在哪个 Android 平台上构建。假设您只安装了 Android 2.3 平台,那么只有这个构建目标应该会出现,并且它应该已经被选中了。
在属性下,在应用名称文本字段中输入 Use GameBoard
。这个人类可读的标题将出现在 Android 设备上。继续,在包名文本字段中输入 com.apress.usegameboard
。该值是包名称空间(遵循与 Java 编程语言中的包相同的规则),所有源代码都将驻留在该名称空间中。如果未选中创建活动复选框,请选中它,并在此复选框旁边的文本字段中输入 UseGameBoard
作为应用的开始活动的名称。未选中此复选框时,文本字段被禁用。最后,在 Min SDK Version 文本字段中输入整数 9
,以确定在 Android 2.3 平台上正确运行UseGameBoard
所需的最低 API 级别。
单击完成。
Eclipse 在 Package Explorer 窗口中创建一个UseGameBoard
节点。完成以下步骤来设置所有文件:
展开 UseGameBoard 节点,然后展开src
节点,再展开com.apress.usegameboard
节点。
双击 UseGameBoard.java 节点(在 com.apress.usegameboard 下面)并用清单 7–4 替换结果窗口中的框架内容。
右键单击“使用游戏板”节点,并从弹出菜单中选择“属性”。
在随后出现的UseGameBoard 的属性对话框中,选择 Android 类别并点击添加按钮。
在弹出的项目选择 对话框中,选择游戏板并点击确定。
点击应用,然后点击确定关闭使用游戏板的属性。
您现在已经准备好运行这个项目了。从菜单栏中选择运行,然后从下拉菜单中选择运行。如果出现运行方式 对话框,选择 Android 应用并点击确定。Eclipse 启动模拟器,安装该项目的 APK,并运行应用,其输出显示在图 7–2 中。
图 7–2。 UseGameBoard
展示了一个蓝白相间的棋盘,可用作跳棋或国际象棋等游戏的背景。
**注意:**如果你有兴趣创建和使用一个基于 Android library 项目的包含一个活动的库,可以查看 Google 的TicTacToe
示例库项目([
developer.android.com/guide/developing/projects/projects-eclipse.html#SettingUpLibraryProject](http://developer.android.com/guide/developing/projects/projects-eclipse.html#SettingUpLibraryProject)
)。
7–5。制图
问题
您正在寻找一个简单的库,让您的应用生成条形图或折线图。
解决办法
虽然有几个 Android 库可以生成图表,但你可能更喜欢 Kidroid.com 的 kiChart 产品([www.kidroid.com/kichart/](http://www.kidroid.com/kichart/)
)的简单性。0.1 版本支持条形图和折线图,Kidroid 承诺在后续版本中添加新的图表类型。
到 kiChart 主页的链接提供了下载kiChart-0.1.jar
(库)和kiChart-Help.pdf
(描述库的文档)的链接。
它是如何工作的
kiChart 的文档指出条形图和折线图支持多个数据系列。此外,它还声明可以将图表导出为图像文件,并且可以定义图表参数(如字体颜色、字体大小、边距等)。
然后,该文档显示了一对由演示应用呈现的示例线图和条形图的截图。这些截图后面是来自这个演示的代码——特别是,LineChart
图表活动类。
LineChart
的源代码揭示了建立图表的基本原理,解释如下:
创建一个扩展com.kidroid.kichart.ChartActivity
类的活动。此活动呈现条形图或折线图。
在活动的onCreate(Bundle)
方法中,创建一个横轴标签的String
数组,并为每组条或每条线创建一个浮点数据数组。
创建一个由com.kidroid.kichart.model.Aitem
(axis item)实例组成的数组,并用存储数据数组的Aitem
对象填充这个数组。每个Aitem
构造函数调用都要求您传递一个android.graphics.Color
值来标识与数据数组相关联的颜色(其显示的值和条或线都以该颜色显示)、一个String
值来将标签与颜色和数据数组以及数据数组本身相关联。
如果想显示条形图,实例化com.kidroid.kichart.view.BarView
类;如果想显示折线图,实例化com.kidroid.kichart.view.LineView
类。
调用该类的public void setTitle(String title)
方法来指定图表的标题。
调用该类的public void setAxisValueX(String[] labels)
方法来指定图表的水平标签。
调用该类的public void setItems(Aitem[] items)
方法来指定图表的数据项数组。
用图表实例作为参数调用setContentView()
来显示图表。
您不必担心为垂直轴选择一系列值,因为 kiChart 会替您完成这项任务。
源代码后面有一个类图,展示了 kiChart 的类并显示了它们之间的关系。例如,com.kidroid.kichart.view.ChartView
是com.kidroid.kichart.view.AxisView
的超类,它超类BarView
和LineView
。
然后记录每个类的属性和ChartView
的public boolean exportImage(String filename)
方法。此方法允许您将图表输出到文件中,如果成功则返回 true,如果不成功则返回 false。
**提示:**要影响垂直轴上显示的值的范围,您需要使用AxisView
的intervalCount
、intervalValue
和valueGenerate
属性。
在实践中,您会发现 kiChart 很容易使用。例如,考虑一个ChartDemo
应用,它的主要活动(也称为ChartDemo
)提供了一个用户界面,让用户通过它的八个文本字段输入 2010 年和 2011 年每一年的季度销售额。主活动还提供了一对按钮,允许用户通过单独的BarChart
和LineChart
活动在条形图或折线图的上下文中查看这些数据。
清单 7–5 展示了ChartDemo
的源代码。
清单 7–5。 ChartDemo
描述输入图表数据值并启动条形图或折线图活动的活动
`// ChartDemo.java
package com.apress.chartdemo;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.EditText;
public class ChartDemo extends Activity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
Button btnViewBC = (Button) findViewById(R.id.viewbc);
AdapterView.OnClickListener ocl;
ocl = new AdapterView.OnClickListener()
{
@Override
public void onClick(View v)
{
final float[] data2010 = new float[4];
int[] ids = { R.id.data2010_1, R.id.data2010_2, R.id.data2010_3,
R.id.data2010_4 };
for (int i = 0; i < ids.length; i++)
{
EditText et = (EditText) findViewById(ids[i]);
String s = et.getText().toString();
try
{
float input = Float.parseFloat(s);
data2010[i] = input;
}
catch (NumberFormatException nfe)
{
data2010[i] = 0;
}
}
final float[] data2011 = new float[4];
ids = new int[] { R.id.data2011_1, R.id.data2011_2,
R.id.data2011_3, R.id.data2011_4 };
for (int i = 0; i < ids.length; i++)
{
EditText et = (EditText) findViewById(ids[i]);
String s = et.getText().toString();
try
{
float input = Float.parseFloat(s);
data2011[i] = input;
}
catch (NumberFormatException nfe)
{
data2011[i] = 0;
}
}
Intent intent = new Intent(ChartDemo.this, BarChart.class);
intent.putExtra("2010", data2010);
intent.putExtra("2011", data2011);
startActivity(intent);
}
};
btnViewBC.setOnClickListener(ocl);
Button btnViewLC = (Button) findViewById(R.id.viewlc);
ocl = new AdapterView.OnClickListener()
{
@Override
public void onClick(View v)
{
final float[] data2010 = new float[4];
int[] ids = { R.id.data2010_1, R.id.data2010_2, R.id.data2010_3,
R.id.data2010_4 };
for (int i = 0; i < ids.length; i++)
{
EditText et = (EditText) findViewById(ids[i]);
String s = et.getText().toString();
try
{
float input = Float.parseFloat(s);
data2010[i] = input;
}
catch (NumberFormatException nfe)
{
data2010[i] = 0;
}
}
final float[] data2011 = new float[4];
ids = new int[] { R.id.data2011_1, R.id.data2011_2,
R.id.data2011_3, R.id.data2011_4 };
for (int i = 0; i < ids.length; i++)
{
EditText et = (EditText) findViewById(ids[i]);
String s = et.getText().toString();
try
{
float input = Float.parseFloat(s);
data2011[i] = input;
}
catch (NumberFormatException nfe)
{
data2011[i] = 0;
}
}
Intent intent = new Intent(ChartDemo.this, LineChart.class);
intent.putExtra("2010", data2010);
intent.putExtra("2011", data2011);
startActivity(intent);
}
};
btnViewLC.setOnClickListener(ocl);
}
}`
ChartDemo
在它的onCreate(Bundle)
方法中实现它的所有逻辑。这个方法主要是设置它的内容视图,并在视图的两个按钮上附加一个点击监听器。
因为这些监听器几乎相同,我们将只考虑附加到viewbc
(查看条形图)按钮的监听器的代码。作为对这个按钮被点击的响应,监听器的onClick(View)
方法被调用来执行以下任务:
用对应于 2010 年数据的四个文本字段的值填充一个data2010
浮点数组。
用对应于 2011 年数据的四个文本字段的值填充一个data2011
浮点数组。
创建一个Intent
对象,将BarChart.class
指定为要启动的活动的类文件。
将data2010
和data2011
数组存储在该对象中,以便可以从BarChart
活动中访问它们。
发起BarChart
活动。
清单 7–6 展示了BarChart
的源代码。
清单 7–6。 BarChart
描述条形图活动
`// BarChart.java
package com.apress.chartdemo;
import com.kidroid.kichart.ChartActivity;
import com.kidroid.kichart.model.Aitem;
import com.kidroid.kichart.view.BarView;
import android.graphics.Color;
import android.os.Bundle;
public class BarChart extends ChartActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
Bundle bundle = getIntent().getExtras();
float[] data2010 = bundle.getFloatArray("2010");
float[] data2011 = bundle.getFloatArray("2011");
String[] arrX = new String[4];
arrX[0] = "2010.1";
arrX[1] = "2010.2";
arrX[2] = "2010.3";
arrX[3] = "2010.4";
Aitem[] items = new Aitem[2];
items[0] = new Aitem(Color.RED, "2010", data2010);
items[1] = new Aitem(Color.GREEN, "2011", data2011);
BarView bv = new BarView(this);
bv.setTitle("Quarterly Sales (Billions)");
bv.setAxisValueX(arrX);
bv.setItems(items);
setContentView(bv);
}
}`
BarChart
首先通过调用其继承的Intent getIntent()
方法获得对传递给它的Intent
对象的引用。然后,它使用这个方法检索对Intent
对象的Bundle
对象的引用,该对象存储数据项的浮点数组。通过调用Bundle
的float[] getFloatArray(String key)
方法来检索每个数组。
BarChart
接下来为图表的 X 轴构建一个标签的String
数组,并创建一个用两个Aitem
对象填充的Aitem
数组。第一个对象存储 2010 年的数据值,并将这些值与红色和作为图例值的 2010 相关联;第二个对象用绿色和图例值 2011 存储 2011 数据值。
在实例化BarView
之后,BarChart
调用这个对象的setTitle(String)
方法来建立图表的标题,setAxisValueX(String[])
方法将 X 轴标签的数组传递给对象,setItems(Aitem[])
方法将Aitem
数组传递给对象。然后将BarView
对象传递给setContentView()
以显示条形图。
**注意:**因为LineChart
与BarChart
几乎相同,所以这个类的源代码不在本章中介绍。您可以通过将BarView bv = new BarView(this);
改为LineView bv = new LineView(this);
来轻松创建LineChart
。此外,为了最佳实践,您可能应该将变量bv
重命名为lv
。还有别忘了把import com.kidroid.kichart.view.BarView;
改成import com.kidroid.kichart.view.LineView;
。
清单 7–7 展示了main.xml
,它描述了构成ChartDemo
用户界面的布局和小部件。
清单 7–7。 main.xml
描述图表演示活动的布局
`