安卓秘籍:问题解决方法(二)
三、通信和网络
许多成功的移动应用的关键是它们与远程数据源连接和交互的能力。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("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 text
Formatted in italics
显示活动时,每个 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("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("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
`
Enter Email: `清单 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("apress.com/resource/we…"); } }`
在这个例子中,我们首先设置一个本地图像(应用图标)作为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("www.bigfoto.com/dog-animal.…");
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()
- 将目标设置为位于外部存储器上的文件 Uri。
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) { mContext = context; mAction = action; mClient = 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 = "search.yahooapis.com/WebSearchSe…";
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 = "search.yahooapis.com/WebSearchSe…"; 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) { mContext = context; mAction = action; mClient = 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) { item.title = buf.toString(); } elseif("link".equals(name) && inItem) { item.link = buf.toString(); } elseif("description".equals(name) && inItem) { item.description = buf.toString(); }
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 = "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
` <manifest xmlns:android="schemas.android.com/apk/res/and…" android:versionCode="1" android:versionName="1.0" package="com.examples.bluetooth"> <application android:icon="@drawable/icon" android:label="@string/app_name" <activity android:name=".ExchangeActivity" 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
` <manifest xmlns:android="schemas.android.com/apk/res/and…" package="com.examples.mapper" android:versionCode="1" android:versionName="1.0">
<activity android:name=".MyActivity" android:label="@string/app_name">
`
请注意为 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: %11.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)。当两者垂直时,该值将处于最小值(9.8。0.0)。因此,平放在桌子上的设备的 X 和 Y 读数都为0.0,z 读数为
示例应用在屏幕底部的 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: %11.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 的许多持久性技术来存储设备上的非易失性数据。