使用MediaStore检索图像
Android的共享内容提供者功能很强大,利用他们我们可以非常容易的创建类型画廊(gallery)的应用。由于内容提供者,本例中是MediaStore,可以在应用间共享,当我们创建使自己的应用显示图像时,不必真去创建一个相机应用并保存图像。既然大多数应用使用缺省的MediaStore,我们可以利用这个来创建我们自己的画廊应用。
从MediaStore选择是非常简单的。我们使用与创建新记录相同的URI,来选择它里面的记录。
Media.EXTERNAL_CONTENT_URI
MediaStore,实际上,所有的内容提供者,都以一种类似数据库的方式在运作。我们从中查询记录,并获得Cursor对象,我们可以用它来迭代结果。
为了查询,我们首先需要创建我们想要返回的列的字符串数组。MediaStore中图像的标准列定义在MediaStore.Images.Media类里。
String[] columns = { Media.DATA, Media._ID, Media.TITLE, Media.DISPLAY_NAME };
为执行实际查询,我们可以用Activity的managedQuery方法。第一个参数是URI,其次是列名数组,再次是限制WHERE子句,WHERE子句的参数,最后,ORDER BY子句。
下面将选择最后一个小时内创建的记录,并按最早到最新进行排序。
首先,我们创建一个名为oneHourAgo的变量,其值为从1970年1月1日到一小时之前所经过的秒数。System.currentTimeMillis()返回从那一天到现在的毫秒数,除以1000我们得到秒数,减去60分*60秒,我们就得到一个小时之前的值。
long oneHourAgo = System.currentTimeMillis()/1000 - (60 * 60);然后,我们把它的值放入一个字符串数组, 该数组将作为WHERE子句的参数。
String[] whereValues = {""+oneHourAgo};
接下来我们选择我们想要返回的列。
String[] columns = { Media.DATA, Media._ID, Media.TITLE, Media.DISPLAY_NAME, Media.DATE_ADDED };
最后我们执行查询。WHERE子句包含了一个?号, 执行时它将由下一个参数的值来代替。如果有多个?号,则传入的数组也必须包含多个值。这里使用的ORDER BY子句,指定返回的数据以创建时间的升序进行排序。
cursor = managedQuery(Media.EXTERNAL_CONTENT_URI, columns, Media.DATE_ADDED + " > ?", whereValues, Media.DATE_ADDED + " ASC");
当然,如果你想返回所有记录,你可以传递null给最后三个参数。
Cursor cursor = managedQuery(Media.EXTERNAL_CONTENT_URI, columns, null, null, null);
返回的游标可以告诉我们所选记录的每个列的索引。
displayColumnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
我们需要索引来从游标中检索字段的值。首先我们调用moveToFirst方法,确保游标有效而且包含了一些查询结果。如果游标没有包含任何结果,这个方法会返回false。我们使用Cursor类的几个方法中的某个来取得实际数据。方法的选择取决于数据的类型,字符串使用getString,整数使用getInt,等等。
if (cursor.moveToFirst()) {
String displayName = cursor.getString(displayColumnIndex);
}
创建图像浏览应用
接下来是一个完整的示例,查询MediaStore取得图像,然后以幻灯片的形式一张接着一张显示给用户。
package com.apress.proandroidmedia.ch1.mediastoregallery; import android.app.Activity; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.provider.MediaStore; import android.provider.MediaStore.Images.Media; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageButton; import android.widget.TextView; public class MediaStoreGallery extends Activity { public final static int DISPLAYWIDTH = 200; public final static int DISPLAYHEIGHT = 200;
不再按照屏幕的大小来加载和显示图像,我们将使用上述的常数来作为图像显示大小。
Cursor cursor; Bitmap bmp; String imageFilePath; int fileColumn; int titleColumn; int displayColumn; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); titleTextView = (TextView) this.findViewById(R.id.TitleTextView); imageButton = (ImageButton) this.findViewById(R.id.ImageButton);
这里我们指定我们想要返回哪些列。参数必须是以字符串数组的方式。在下一行,我们将数组传递给managedQuery方法。
String[] columns = { Media.DATA, Media._ID, Media.TITLE, Media.DISPLAY_NAME }; cursor = managedQuery(Media.EXTERNAL_CONTENT_URI, columns, null, null, null);
对于我们想从Cursor对象中获取数据的列,我们必须知道他们每个的索引。在本例中,我们从Media.DATA切换到MediaStore.Images.Media.DATA.这仅仅是为了说明他们是相同的。使用Media.DATA仅仅是一个简捷方式,因为我们有一条包含它的import语句:android.provider.MediaStore.Images.Media。
fileColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.TITLE); displayColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME);
当我们运行查询并取得返回的Cursor对象之后,我们调用Cursor对象的moveToFirst检测它是否包含有查询结果。
if (cursor.moveToFirst()) { //titleTextView.setText(cursor.getString(titleColumn)); titleTextView.setText(cursor.getString(displayColumn)); imageFilePath = cursor.getString(fileColumn); bmp = getBitmap(imageFilePath); //显示图像 imageButton.setImageBitmap(bmp); }
我们为ImageButton设定一个新的事件监听类OnClickListener,它调用Cursor对象的moveToNext方法。这将迭代整个返回记录,取出并显示每一张返回的图像。
imageButton.setOnClickListener( new OnClickListener() { public void onClick(View v) { if (cursor.moveToNext()) { //titleTextView.setText(cursor.getString(titleColumn)); titleTextView.setText(cursor.getString(displayColumn)); imageFilePath = cursor.getString(fileColumn); bmp = getBitmap(imageFilePath); imageButton.setImageBitmap(bmp); } } }); }
在这有个名为getBitmap的函数,它封装了图像的缩放和加载。我们之前讨论过,对图像的这些处理是为了避免图像显示引起内存方面的问题。
private Bitmap getBitmap(String imageFilePath) { // 加载图像尺寸,非图像自身 BitmapFactory.Options bmpFactoryOptions = new BitmapFactory.Options(); bmpFactoryOptions.inJustDecodeBounds = true; Bitmap bmp = BitmapFactory.decodeFile(imageFilePath, bmpFactoryOptions); int heightRatio = (int) Math.ceil(bmpFactoryOptions.outHeight / (float)DISPLAYHEIGHT); int widthRatio = (int) Math.ceil(bmpFactoryOptions.outWidth / (float)DISPLAYWIDTH); Log.v("HEIGHTRATIO", "" + heightRatio); Log.v("WIDTHRATIO", "" + widthRatio); // 如果两个比值都大于1,图像的某一边大于屏幕 if (heightRatio > 1 && widthRatio > 1) { if (heightRatio > widthRatio) { // 高度比较大,根据它进行缩放 bmpFactoryOptions.inSampleSize = heightRatio; } else { // 宽度比较大,根据它进行缩放 bmpFactoryOptions.inSampleSize = widthRatio; } } // 真正解码 bmpFactoryOptions.inJustDecodeBounds = false; bmp = BitmapFactory.decodeFile(imageFilePath,bmpFactoryOptions); return bmp; } }
随同activity的布局XML如下,它应该放在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" > <ImageButton android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/ImageButton"> </ImageButton> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/TitleTextView" android:text="Image Title"/> </LinearLayout>
内部元数据
一般来说,EXIF数据是非常技术导向的;标准中的大部分标签都是关于图像自身采集的,比如曝光时间,快门速度。然而,还是有些标签对我们而言是有意义的,我们可以填写或者改动。下面是其中一些:
UserComment: 用户写的评论
ImageDescription: 标题
Artist: 图片的创建者或者拍摄者
Copyright: 图片的版权所有者
Software: 用于创建图片的软件
幸运的是,Android给我们提供了一个很好的方法用于读写EXIF数据。ExifInterface是其大类。
这是如何用ExifInterface从图像文件中读取特定的EXIF数据:
ExifInterface ei = new ExifInterface(imageFilePath); String imageDescription = ei.getAttribute("ImageDescription"); if (imageDescription != null) { Log.v("EXIF", imageDescription); }
这是如何用ExifInterface将EXIF数据保持到一个图像文件中去:
ExifInterface ei = new ExifInterface(imageFilePath); ei.setAttribute("ImageDescription","Something New");
ExifInterface包含一组常量,定义了图像的典型元数据,这些数据由相机应用在拍摄图像时加入进来。
EXIF规格的最新版本是2.3, 2010年4月发布。这里提供在线下载:http://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf.
总结
在本章中,我们研究了Android的图像采集和储存基础知识。我们看到了如何在Android中使用强大的内置相机应用程序以及如何通过一个Intent,有效地发挥其功能。我们看到了相机应用提供的精巧统一的接口,如何帮助其他Android应用增加图像采集功能。
我们还看到了处理大图像时,需要注意内存的使用。我们学到BitmapFactory类可以帮助我们加载图像的缩小版本以节约内存。对内存的关注提醒我们,手机不是桌面电脑,有看似无限的内存。
我们练习了Android内置图像内容提供者,MediaStore的使用。我们学会如何用它来将图像保存到设备上的标准位置,以及如何快速建立一个应用程序,使用它来查询已拍摄的图像。
最后,我们看了一下如何用EXIF标准来关联图像的特定元数据。EXIF具有便携性且用于各类设备我软件应用程序。
这给了我们一个非常不错的起点,去探索更多我们能在Android上做的媒体有关的东西。
我对此充满期待期待!