安卓秘籍:问题解决方法(四)
六、与系统交互
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- 参照 UTC 时间的报警时间。
RTC_WAKEUP- 参考 UTC 时间的闹钟时间,如果设备处于睡眠状态,将唤醒设备进行触发。
以下参数(分别)指的是警报第一次触发的时间和重复的时间间隔。因为选择的警报类型是 ELAPSED_REALTIME,开始时间也必须相对于经过时间;SystemClock.elapsedRealtime()以此格式提供当前时间。
示例中的警报被注册为在按下按钮 5 秒后触发,之后每隔 5 秒触发一次。每五秒钟,屏幕上会出现一个带有当前时间值的Toast,即使该应用不再运行或不在用户面前。当用户显示活动并按下停止按钮时,任何与我们的PendingIntent匹配的未决警报都会被立即取消…停止Toast的流程
一个更精确的例子
如果我们想安排一个闹铃在特定的时间发生呢?也许每天早上 9 点一次?用一些稍微不同的参数设置AlarmManager可以实现这一点。参见清单 6–6。
清单 6–6。 精确报警
` long oneDay = 2436001000; //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
` <manifest xmlns:android="schemas.android.com/apk/res/and…" package="com.examples.sticky" android:versionCode="1" android:versionName="1.0">
<activity android:name=".ReportActivity" android:label="@string/app_name"> `
**提醒:**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) { trackerService = null; } }; }`
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)) {
title = incoming.getStringExtra(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[] { ContactsContract.Contacts.DISPLAY_NAME },
newint[] { android.R.id.text1 });
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[] { CommonDataKinds.Email.DATA }, ContactsContract.Data.CONTACT_ID + " = " + selectedId, null, null); // Gather phone data from phone table Cursor phone = getContentResolver().query( CommonDataKinds.Phone.CONTENT_URI, new String[] { CommonDataKinds.Phone.NUMBER }, ContactsContract.Data.CONTACT_ID + " = " + selectedId, null, null); // Gather addresses from address table Cursor address = getContentResolver().query( CommonDataKinds.StructuredPostal.CONTENT_URI, new String[] { CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS }, 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[] { ContactsContract.Contacts.DISPLAY_NAME }, newint[] { android.R.id.text1 });
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.Contacts._ID }, 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 = rowsquareDim;
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描述图表演示活动的布局
` <TableLayout xmlns:android="schemas.android.com/apk/res/and…" android:layout_width = "fill_parent" android:layout_height="fill_parent" android:stretchColumns="*"> <TextView android:text="2010" android:layout_gravity="center"/> <TextView android:text="2011" android:layout_gravity="center"/>
<EditText android:id="@+id/data2010_1"
android:inputType="numberDecimal"
android:maxLines="1"/> <EditText android:id="@+id/data2011_1"
android:inputType="numberDecimal"
android:maxLines="1"/>
<EditText android:id="@+id/data2010_2" android:inputType="numberDecimal" android:maxLines="1"/> <EditText android:id="@+id/data2011_2" android:inputType="numberDecimal" android:maxLines="1"/>
<EditText android:id="@+id/data2010_3" android:inputType="numberDecimal" android:maxLines="1"/> <EditText android:id="@+id/data2011_3" android:inputType="numberDecimal" android:maxLines="1"/>
<EditText android:id="@+id/data2010_4" android:inputType="numberDecimal" android:maxLines="1"/> <EditText android:id="@+id/data2011_4" android:inputType="numberDecimal" android:maxLines="1"/>
<Button android:id="@+id/viewbc" android:text="View Barchart"/> <Button android:id="@+id/viewlc" android:text="View Linechart"/> `
main.xml通过<TableLayout>标签描述了一个表格布局,其中用户界面分为六行三列。这个标签的layout_width和layout_height属性的"fill_parent"赋值告诉这个布局占据活动的整个屏幕。对这个标签的stretchColumns属性的"*"赋值告诉这个布局给每一列一个相同的宽度。
**注:**可伸缩柱是一种可以在宽度上扩展以适应任何可用空间的柱。要指定哪些列是可伸缩的,请将一个以逗号分隔的从 0 开始的整数列表分配给stretchColumns。例如,"0, 1"指定列 0(最左边的列)和列 1 是可拉伸的。"*"赋值表示所有列都是可拉伸的,这使得它们具有相同的宽度。
嵌套在<TableLayout>和它的</TableLayout>伙伴中的是一系列的<TableRow>标签。每个<TableRow>标签描述了表格布局中单个行的内容,这些内容是零个或多个视图的变体(例如TextView和EditText),其中每个视图构成一列。
注:为简洁起见,字符串值直接存储在main.xml中,而不是存储在单独的strings.xml文件中。把它当作一个引入strings.xml的练习,并用对存储在strings.xml中的字符串的引用替换这些文字字符串。
清单 7–8 展示了这个应用的AndroidManifest.xml文件,它描述了这个应用及其活动。
清单 7–8。 AndroidManifest.xml为ChartDemo App 汇集一切
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.apress.chartdemo" android:versionCode="1" android:versionName="1.0"> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".ChartDemo" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> **<activity android:name=".BarChart"/>** **<activity android:name=".LineChart"/>** </application> <uses-sdk android:minSdkVersion="9" /> </manifest>
在清单中包含每个BarChart和LineChart活动的<activity>标记是很重要的。否则,运行时对话框会显示以下消息:“The application Chart Demo (process com.apress.chartdemo) has stopped unexpectedly. Please try again.”
图 7–3 显示了ChartDemo的主要活动,每个季度输入样本值。
图 7-3。 ChartDemo允许您输入八个数据值,并选择通过条形图或折线图显示这些值。
输入上述数据值后,点击查看条形图按钮启动BarChart活动,显示图 7–4 中所示的条形图。
图 7–4。 BarChart通过一系列彩色条显示每个数组的数据值。
除了呈现一个条形图,图 7–4 揭示了正在使用 kiChart 的试用版。你需要联系 Kidroid.com,了解许可以及如何获得不显示此消息的 kiChart 版本。
7–6 岁。实用推送消息
问题
谷歌的云到设备消息传递(C2DM)框架([code.google.com/android/c2dm/index.html](http://code.google.com/android/c2dm/index.html))旨在实现设备的推送消息传递,它有许多缺点,这些缺点可能会影响它作为推送消息传递的实用解决方案。你的应用需要一个更通用的推送解决方案。
谷歌 C2DM 的局限性
C2DM 是谷歌开发的一项技术,通过可扩展消息和存在协议(XMPP)在 Android 设备上运行,XMPP 是聊天客户端的常见实现。通过进一步的观察,C2DM 有许多必需的属性,这些属性通常会降低它在应用中的有用性:
- 要求最低 API 级别为 8: 虽然这一限制不会永远成为一个重大限制,但现在希望在运行 2.2 之前版本的 Android 设备上支持推送消息的应用将无法使用 C2DM。
- **需要设备上的 Google 帐户和 Google API:**C2DM 在 GTalk 聊天服务创建的 XMPP 通道上运行。如果用户在不包含 Google APIs(以及 GTalk 应用)的 Android 设备上运行,或者如果他们没有在设备上输入有效的 Google 帐户,您的应用将无法在该设备上注册 C2DM 消息。
- **利用 HTTP POST 进行主机应用和 C2DM 服务器之间的事务:**从应用的服务器端,要发送到设备的消息通过对每条消息使用单独的 HTTP POST 请求被传递到 C2DM 服务器。随着需要发送的消息数量的增加,这种机制变得越来越慢,以至于 C2DM 在某些时间关键的应用中可能不是可行的选择。
解决办法
利用 IBM 的 MQTT 库在您的应用中实现轻量级推送消息。MQTT 客户端库由 IBM 以纯 Java 实现的形式提供,这意味着它可以在任何 Android 设备上使用,没有特定 API 级别的限制。
MQTT 系统由三个主要组件组成:
- **客户端应用:**在设备上运行,并向消息代理注册一组给定的“主题”来接收消息。
- **消息代理:**处理客户端的注册,并根据客户端的“主题”将来自服务器应用的消息分发到每个客户端
- **服务器应用:**负责向代理发布消息。
邮件按主题过滤。主题以树形格式定义,由路径字符串表示。通过提供适当的路径,客户端可以订阅特定的主题或子主题组。例如,假设我们为应用定义了两个主题,如下所示:
examples/one examples/two
客户端可以通过订阅精确的完整路径字符串来订阅任一主题。但是,如果客户希望订阅这两个主题(以及该组中稍后可能创建的任何其他主题),则可以通过以下方式方便地进行订阅:
examples/#
通配符“#”表示该客户对示例组中的所有主题都感兴趣。
在这个菜谱中,我们将重点关注在 Android 设备上使用 MQTT 库实现客户端应用。IBM 为其他组件的开发和测试提供了优秀的工具,我们也将在这里展示这些工具。
它是如何工作的
MQTT Java 库可以从 IBM 的以下位置免费下载:www-01.ibm.com/support/docview.wss?uid=swg24006006。除了库 JAR 之外,下载档案还包含示例代码、API Javadoc 和使用文档。
从下载档案中找到wmqtt.jar文件。这是 Android 项目中必须包含的库。按照惯例,这意味着应该在您的项目目录中创建一个/libs目录,并且应该在那里插入这个 JAR。
为了测试您的客户机实现,IBM 提供了非常小的消息代理(RSMB)。RSMB 可以在以下位置下载:[www.alphaworks.ibm.com/tech/rsmb](http://www.alphaworks.ibm.com/tech/rsmb)。
RSMB 是一个多平台下载,包括用于消息代理和发布消息的应用的命令行工具。IBM 为此工具提供的许可证禁止在生产环境中使用它;此时,您将需要推出自己的解决方案,或者使用众多可用的开源实现之一。然而,对于移动客户端的开发,RSMB 再好不过了。
客户端示例
因为监视传入的推送消息是一个不确定的、长期的操作,所以让我们看一个将基本功能放入服务的例子。
**注意:**提醒一下,您的项目目录中应该有libs/wmqtt.jar,并在您的项目构建路径中被引用。
清单 7–9 展示了一个示例 MQTT 服务的源代码。
清单 7–9。 MQTT 示例服务
`//ClientService.java package com.apress.pushclient;
import android.app.AlarmManager; import android.app.PendingIntent; import android.app.Service; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.widget.Toast; //Imports required from the MQTT Library JAR import com.ibm.mqtt.IMqttClient; import com.ibm.mqtt.MqttClient; import com.ibm.mqtt.MqttException; import com.ibm.mqtt.MqttPersistenceException; import com.ibm.mqtt.MqttSimpleCallback;
public class ClientService extends Service implements MqttSimpleCallback {
//Location where broker is running privatestaticfinal String HOST = HOSTNAME_STRING_HERE; privatestaticfinal String PORT = "1883"; //30 minute keep-alive ping privatestaticfinalshortKEEP_ALIVE = 60 * 30; //Unique identifier of this device privatestaticfinal String CLIENT_ID = "apress/"+System.currentTimeMillis(); //Topic we want to watch for privatestaticfinal String TOPIC = "apress/examples";
privatestaticfinal String ACTION_KEEPALIVE = "com.examples.pushclient.ACTION_KEEPALIVE";
private IMqttClient mClient; private AlarmManager mManager; private PendingIntent alarmIntent;
@Override public void onCreate() { super.onCreate(); mManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
Intent intent = new Intent(ACTION_KEEPALIVE);
alarmIntent = PendingIntent.getBroadcast(this, 0, intent, 0); registerReceiver(mReceiver, new IntentFilter(ACTION_KEEPALIVE));
try { //Format: tcp://hostname@port String connectionString = String.format("%s%s@%s", MqttClient.TCP_ID, HOST, PORT); mClient = MqttClient.createMqttClient(connectionString, null); } catch (MqttException e) { e.printStackTrace(); //Can't continue without a client stopSelf(); } }
@Override public void onStart(Intent intent, int startId) { //Callback on Android devices prior to 2.0 handleCommand(intent); }
@Override publicint onStartCommand(Intent intent, int flags, int startId) { //Callback on Android devices 2.0 and later handleCommand(intent); //If Android kills this service, we want it back when possible return START_STICKY; }
private void handleCommand(Intent intent) { try { //Make a connection mClient.connect(CLIENT_ID, true, KEEP_ALIVE); //Target MQTT callbacks here mClient.registerSimpleHandler(this); //Subscribe to a topic String[] topics = new String[] { TOPIC }; //QoS of 0 indicates fire once and forget int[] qos = newint[] { 0 }; mClient.subscribe(topics, qos);
//Schedule a ping scheduleKeepAlive(); } catch (MqttException e) { e.printStackTrace(); } }
@Override public void onDestroy() { super.onDestroy(); unregisterReceiver(mReceiver); unscheduleKeepAlive();
if(mClient != null) {
try {
mClient.disconnect();
mClient.terminate(); } catch (MqttPersistenceException e) {
e.printStackTrace();
}
mClient = null;
}
}
//Handle incoming message from remote private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { String incoming = (String)msg.obj; Toast.makeText(ClientService.this, incoming, Toast.LENGTH_SHORT).show(); } };
//Handle ping alarms to keep the connection alive private BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if(mClient == null) { return; } //Ping the MQTT service try { mClient.ping(); } catch (MqttException e) { e.printStackTrace(); } //Schedule the next alarm scheduleKeepAlive(); } };
private void scheduleKeepAlive() { long nextWakeup = System.currentTimeMillis() + (KEEP_ALIVE * 1000); mManager.set(AlarmManager.RTC_WAKEUP, nextWakeup, alarmIntent); }
private void unscheduleKeepAlive() { mManager.cancel(alarmIntent); }
/* MqttSimpleCallback Methods */
@Override public void connectionLost() throws Exception { mClient.terminate(); mClient = null; stopSelf(); }
@Override
public void publishArrived(String topicName, byte[] payload, int qos, boolean retained) throws Exception {
//Be wary of UI related code here!
//Best to use a Handler for UI or Context operations StringBuilder builder = new StringBuilder();
builder.append(topicName);
builder.append('\n');
builder.append(new String(payload));
//Pass the message up to our handler
Message receipt = Message.obtain(mHandler, 0, builder.toString());
receipt.sendToTarget();
}
/Unused method/ //We are not using this service as bound //It is explicitly started and stopped with no direct connection @Override public IBinder onBind(Intent intent) { returnnull; } }`
**重要提示:**这个Service很可能会与远程服务器通信,因此您必须在应用清单中声明android.permission.INTERNET,以及带有<service>标签的Service本身。
为了子类化Service,必须提供onBind()的实现。在这种情况下,我们的例子不需要提供一个Binder接口,因为活动永远不需要直接挂钩到调用方法中。因此,这个必需的方法只返回 null。这个Service被设计成接收启动和停止的明确指令,在其间运行一段不确定的时间。
当Service被创建时,一个MqttClient对象也被使用createMqttClient()实例化;这个客户机将消息代理主机的位置作为一个字符串。连接字符串的格式为tcp://hostname@port。在本例中,选择的端口号是 1883,这是 MQTT 通信的默认端口号。如果您选择不同的端口号,您应该验证您的服务器实现是否在匹配的端口上运行。
从这一点开始,Service保持空闲,直到发出启动命令。一旦收到开始命令(通过调用Context.startService()从外部发出),将调用onStart()或onStartCommand()(取决于设备上运行的 Android 版本)。在后一种情况下,服务返回START_STICKY,这是一个常量,告诉系统应该让这个服务继续运行,如果它因为内存原因被提前终止,就重新启动它。
一旦启动,服务将向 MQTT 消息代理注册,传递一个惟一的客户机 ID 和一个保活时间。为了简单起见,这个例子根据服务创建时的当前时间来定义客户机 ID。在生产中,更独特的标识符,如 Wi-Fi MAC 地址或TelephonyManager.getDeviceId()可能更合适,记住这两种选择都不能保证出现在所有设备上。
keep-alive 参数是时间(以秒为单位),代理应该使用该时间使到该客户端的连接超时。为了避免这种超时,客户应该发布消息或定期 ping 代理。我们将很快更全面地讨论这项任务。
在启动过程中,客户端还订阅了一个主题。注意,subscribe()方法将数组作为参数;一个客户端可以在一个方法调用中订阅多个主题。每个主题还订阅有请求的服务质量(QoS)值。对移动设备请求的最委婉的值是零,告诉代理只发送一次消息而不需要确认。这样做减少了代理和设备之间所需的握手次数。
随着连接的激活和注册,来自远程代理的任何传入消息都将导致对publishArrived()的调用,并传递关于该消息的数据。这个方法可以在任何由MqttClient创建和维护的后台线程上调用,所以不要在这里直接做任何与主线程相关的事情是很重要的。在本例中,所有传入的消息都被传递到一个本地的Handler,以保证结果Toast被发送到主线程上进行显示。
实现 MQTT 客户机时需要一项维护任务,那就是 ping 代理以保持连接活动。为了完成这个任务,Service向AlarmManager注册,以根据匹配保活参数的时间表触发广播。即使设备当前处于睡眠状态,也必须完成该任务,因此每次使用AlarmManager.RTC_WAKEUP设置闹铃。当每个警报触发时,Service简单地调用MqttClient.ping()并安排下一次保活更新。
由于这一要求的持续性质,为保活定时器选择低频间隔是谨慎的;在这个例子中,我们选择了 30 分钟。该定时器值代表了减少设备上所需更新的频率(以节省功率和带宽)和远程代理意识到远程设备不再存在并超时之前的等待时间之间的平衡。
当不再需要推送服务时,对Context.stopService()的外部调用将导致对onDestroy()的调用。在这里,Service拆除 MQTT 连接,删除任何未决的警报,并释放所有资源。作为MqttSimpleCallback接口的一部分实现的第二个回调是onConnectionLost(),表示意外的断开。在这些情况下,Service会像手动停止请求一样自行停止。
测试客户端
为了测试设备的消息传递,您需要在您的机器上启动一个 RSMB 实例。从命令行中,导航到您解压缩下载的位置,然后导航到与您的计算机平台(Windows、Linux、Mac OS X)匹配的目录。从这里,只需执行broker命令,代理服务将开始在您的机器上运行,位于localhost:1883:
CWNAN9999I Really Small Message Broker CWNAN9997I Licensed Materials - Property of IBM CWNAN9996I Copyright IBM Corp. 2007, 2010 All Rights Reserved …
CWNAN0014I MQTT protocol starting, listening on port 1883
此时,您可以连接到服务并发布消息或注册接收消息。为了对这个Service进行测试,清单 7–10 和清单 7–11 创建了一个简单的Activity,可以用来启动和停止服务。
清单 7–10。 res/menu/home.xml
`
`清单 7–11。 活动控制 MQTT 服务
`//ClientActivity.java package com.apress.pushclient;
import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem;
public class ClientActivity extends Activity {
private Intent serviceIntent;
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
serviceIntent = new Intent(this, ClientService.class); }
@Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.home, menu); return true; }
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case R.id.menu_start:
startService(serviceIntent);
return true;
case R.id.menu_stop:
stopService(serviceIntent);
return true;
} return super.onOptionsItemSelected(item);
}
}`
清单 7–11 创建了一个Intent,它将被两个菜单选项用来随意启动和停止服务(参见图 7–5)。通过按下菜单按钮并选择“启动服务”,MQTT 连接将启动并为设备注册主题为“进程/示例”的消息
图 7–5。 活动控制服务
注意:示例服务中的HOST值需要指向运行您的 RSMB 实例的机器。即使你在同一台机器上的模拟器中测试,这个值也是而不是 localhost!至少,您必须将模拟器或设备指向运行您的代理的机器的 IP 地址。
Android 设备成功注册了来自代理的推送消息后,打开另一个命令行窗口,导航到执行broker的同一个目录。另一个命令stdinpub可用于连接到代理实例,并将消息发布到设备。从命令行键入以下命令:
stdinpub apress/examples
该命令将注册一个客户端,以发布主题与我们的示例相匹配的消息。您将看到以下结果:
Using topic apress/examples Connecting
现在你可以输入任何你喜欢的信息,然后回车。按下 Enter 键后,消息将被发送到代理,并被推送到注册的设备。尽可能多次这样做,然后使用 CTRL-C 退出程序。CTRL-C 还将终止代理服务。
提示:【RSMB】还包含第三个命令stdoutsub,用于向您的本地代理服务订阅一组主题。这个命令让您完全关闭循环,并测试问题是发生在测试套件中还是您的 Android 应用中。
总结
聪明的 Android 开发者通过利用库来更快地将他们的应用交付给市场,库通过提供先前创建和测试的代码来减少开发时间。
本章的初始秘籍向您介绍了创建和使用您自己的库的主题。具体来说,您学习了如何创建和使用 Java 库 jar,其代码仅限于 Java 5(或更早版本)API 和 Android 库项目。
尽管您可能会创建自己的库来避免重复劳动,但您可能也需要使用其他人的库。例如,如果您需要一个简单的图表库,您可能想看看 kiChart,它有助于条形图和折线图的显示。
如果你正在使用云,你可能会决定使用谷歌的 C2DM 框架。但是,因为这个框架有许多缺点(比如要求最低 API 级别为 8),所以您可以考虑利用 IBM 的 MQTT 库在您的应用中实现轻量级的推送消息。