Android中图片的读取,修改,显示和保存涉及到的类大致如图所示。
在读取图片文件时,先将图片文件转换为InputStream对象,然后通过BitmapFactory将其转换为Bitmap对象。
在图片保存时,先将Bitmap对象转换为OutputStream对象,然后再将OutputStream输出到文件中。
如果要对图片进行修改,可以通过将Bitmap对象转换为颜色数组(int[])来修改,也可以通过Canvas来修改。此外Bitmap类提供了一个createBitmap的静态方法,可以对Bitmap对象做一些转换。
显示图片时,可以将Bitmap对象转换为Drawable对象,然后设置给ImageView。
本文介绍图片文件的读取。
目前在Android中支持的图片文件格式如下。其中WebP格式是从Android4.0开始支持,但在Android4.0到Android4.2.1之间的Android系统不支持无损压缩和有透明度的WebP格式图片,Android4.2.1之后才开始支持所有的WebP格式图片。
下图截取自http://developer.android.com/guide/appendix/media-formats.html
1、读取SD卡中的图片到InputStream
对SD卡中的文件,获取到文件的路径之后便可以通过FileInputStream类来生成InputStream。FileInputStream类是InputStream类的派生类。代码示例如下。
FileInputStream stream = new FileInputStream(fileName);
2、读取resource中的图片到InputStream
对resource/drawable目录下的图片,每个文件都会在R文件中生成一个对应的整数id,通过“R.drawable.文件名”来得到id值,然后通过Resource类的openRawResource()来得到该文件对应的InputStream对象。代码示例如下。
InputStream strem = getResources().openRawResource(R.drawable.name);
3、读取assets中的图片到InputStream
对assets目录中的图片,先通过getAssets()得到AssetManager对象,然后通过AssetManager对象的open方法来打开文件,得到该文件对应的InputStream对象。代码示例如下。
InputStream strem = getAssets().open("apple.png");
注意:如果图片在assets根目录,读取时只需要加图片文件名,不需要加/assets/,图片文件名需要包含扩展名。如果图片在assets的子目录中,需要加上子目录的路径,例如apple.png在/assert/fruit/spring/目录中,则open参数为”/fruit/spring/apple.png”。
4、 通过Uri读取图片到InputStream
Uri全写是Universal Resource Identifier(通用资源标志符),Android系统中所有资源都可以用Uri来表示。在获取到资源的Uri后可以通过ContentResolver将其解析成InputStream对象。代码示例如下。
InputStream strem = getContentResolver().openInputStream(uri);
openInputStream()核心代码如下。
String scheme = uri.getScheme();
if (SCHEME_ANDROID_RESOURCE.equals(scheme)) {
OpenResourceIdResult r = getResourceId(uri);
InputStream stream = r.r.openRawResource(r.id);
return stream;
} else if (SCHEME_FILE.equals(scheme)) {
return new FileInputStream(uri.getPath());
} else {
AssetFileDescriptor fd = openAssetFileDescriptor(uri, "r", null);
return fd.createInputStream();
}
这里openInputStream()内先判断Uri的scheme类型,如果是resource类型,则先调用getResourceId()得到该Uri对应的资源id,然后调用openRawResource()读取文件到InputStream对象。如果是文件类型,则调用uri.getPath()得到文件路径,然后调用FileInputStream()读取文件到InputStream对象。对其他类型(即assets类型)则调用openAssetFileDescriptor()先得到一个AssetFileDescriptor对象,然后再得到一个InputStream对象。
可以看到,通过Uri读取图片的流程和上述根据类型直接读取大体是一样的(assets类型稍有不同)。
通过BitmapFactory的decodeStream()方法可以将InputStream转换为Bitmap对象。代码示例如下。
Bitmap bitmap = BitmapFactory.decodeStream(stream);
上述流程是先将图片文件读取到InputStream中,然后将InputStream转换为Bitmap对象,需要两步。BitmapFactory提供了一些静态类可以直接将图片文件转换为Bitmap对象。
直接读取SD卡中的图片到Bitmap示例代码如下。
Bitmap bitmap = BitmapFactory.decodeFile(fileName);
直接读取resource中的图片到Bitmap示例代码如下。
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), id);
BitmapFactory并没有提供直接读取assets中图片的api。
查看BitmapFactory源码可以看到,BitmapFactory.decodeFile()和BitmapFactory.decodeResource()两个api都是先将SD卡或resource中的图片读取到InputStream,然后再调用BitmapFactory.decodeStream()来解析。由此可以看出,直接读取图片到Bitmap和先读取图片到InputStream,然后将InputStream转换为Bitmap本质上是一样的,只是BitmapFactory将这两步封装在一起。因此,如果要将非assets中的图片文件转换为Bitmap,一般就直接使用BitmapFactory即可。
BitmapFactory在decode时可以传递一个Options参数,用来设置转换到Bitmap时的一些参数。
BitmapFactory.Options中包含了一系列的public成员变量,每个成员变量代表一个转换参数。这里列出了BitmapFactory.Options类中定义的全部public成员变量。在BitmapFactory.Options类的注释中详细的描述了每个成员变量的含义。这里只介绍几个常用的成员变量的用法。
public static class Options {
public Bitmap inBitmap;
public boolean inMutable;
public boolean inJustDecodeBounds;
public int inSampleSize;
public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;
public boolean inPremultiplied;
public boolean inDither;
public int inDensity;
public int inTargetDensity;
public int inScreenDensity;
public boolean inScaled;
public boolean inPurgeable;
public boolean inInputShareable;
public boolean inPreferQualityOverSpeed;
public int outWidth;
public int outHeight;
public String outMimeType;
public byte[] inTempStorage;
public boolean mCancel;
}
inJustDecodeBounds:此变量默认为false。当此变量被设置为true时,表示只对图片的Bound(图片的范围,也就是长宽包含的像素大小)进行解析,不生成Bitmap对象。在BitmapFactory.Options定义中可以看到有三个out开头的成员变量,outWidth,outHeight和outMimeType。当inJustDecodeBounds设置为true时,会分别将图片的宽度,高度和Mime类型保存到这三个成员变量中。
inSampleSize:inSampleSize是一个整数,表示对图片像素的取样比例,即图片中每inSampleSize个像素取样后对应Bitmap中的一个像素。例如inSampleSize=2,表示图片中每两个像素对应Bitmap中的一个像素,转换后Bitmap的宽和高包含的像素个数是原图片的1/2,总像素个数是原图的1/4。inSampleSize=4,表示图片中每四个像素对应Bitmap中的一个像素,转换后Bitmap的宽和高包含的像素个数是原图片的1/4,总像素个数是原图的1/16。如果它小于1等于1,则表示完全取样,即图片中每个像素都会被提取保存到Bitmap中,转换后Bitmap的宽和高包含的像素个数和原图片相等。图片取样时只能按照2的整数次幂来取样,比如1,2,4,8,16……,如果inSampleSize不是2的整数次幂,则实际取样时使用和inSampleSize最接近的2的整数次幂来取样,比如inSampleSize= 10,和10最接近的2的整数次幂是8,因此会每8个像素取样一次。
inPreferredConfig:表示图片中每个像素的保存方式。默认为Bitmap.Config.ARGB_8888,表示每个像素需要包含Alpha,R,G,B四个通道的信息,每个通道用8位来表示,也就是需要4个字节来保存一个像素。
1、WebP图片格式问题
虽然Android官方描述是在4.0之后开始支持WebP格式,但在4.0到4.2.1不支持有透明度设置和无损压缩的WebP格式图片。所以如果简单的判断,图片如果是WebP格式,且是4.0以上的系统,就通过BitmapFactory来解析,很可能会出现有些图片能解析,有些又不能解析的情况。此外,还有一些特殊机型,例如NokiaXL虽然是Android4.1的系统,但并不支持WebP编解码。因此,最好是从4.2系统开始用BitmapFactory来解析WebP格式的图片。
2、解析大图时的OOM问题
Android为每个应用分配的堆内存空间是有限的,如果应用请求的内存超过了限制,就会导致OOM。根据手机内存大小的不同,一般应用能够使用的堆内存在几十兆到两三百兆之间。然后应用在读取图片文件时非常消耗内存的,一张手机相机拍出的照片都有3M左右,如果每张图都直接读取,即使不造成OOM,也会导致频繁GC。还有些超大图一张就有几个G,如果直接读取肯定会导致OOM。为了避免读取大图时造成的OOM问题,并减少内存消耗,一般在读取图片时都需要根据图片尺寸来取样。也就是设置BitmapFactory.Options的inSampleSize属性。示例代码如下。
Options options = new BitmapFactory.Options();
//设置inJustDecodeBounds为true表示只获取大小,不生成Btimap
options.inJustDecodeBounds = true;
//解析图片大小
InputStream stream = getContentResolver().openInputStream(uri);
BitmapFactory.decodeStream(stream, null, options);
stream.close();
int width = options.outWidth;
int height = options.outHeight;
int ratio = 0;
//如果宽度大于高度,交换宽度和高度
if (width > height) {
int temp = width;
width = height;
height = temp;
}
//计算取样比例
it sampleRatio = Math.max(width/900, height/1600);
//定义图片解码选项
Options options = new Options();
options.inSampleSize = sampleRatio;
//读取图片,并将图片缩放到指定的目标大小
InputStream stream = getContentResolver().openInputStream(uri);
Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options);
stream.close();