Android多媒体开发 Pro Android Media 第一章 Android图像编程入门 4

使用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(exchangeable image file format), 是在图像文件中保存元数据的标准方式。很多相机和桌面应用都支持EXIF数据。因为EXIF数据是作为文件的一部分,它不会在文件从一个地方传递到其他地方时丢失。比如,从Android设备的SD卡拷贝一个文件到家中电脑,EXIF数据保持不变。如果你用某个应用程序,比如iPhoto打开这个文件,这些数据就会呈现出来。

一般来说,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上做的媒体有关的东西。

我对此充满期待期待!

Android多媒体开发 Pro Android Media 第一章 Android图像编程入门 4

上一篇:Android-部分可编辑的EditText


下一篇:Android上用模板方法模式实现具有自动重用View功能的Adapter