Android 处理大图问题
背景
无论在现实开发中,还是面试中,这个问题都会经常遇到。
具体情况可以分为两种
-
图片的大小很大,但是需要在android中显示的区域却没有图片真正大小那么大。
比如一个高清图片作为头像,图片的大小是1M,10241024。但是在手机里只需要显示8080的大小。
比如著名的清明上河图(30000*926)如果只是要显示缩略图,就不必加载原图
-
图片的大小很大,需要在Android中可以显示原图大小。
比如要查看高清头像的原图
比如著名的清明上河图(30000*926)如果显示原图这样的大图加载到内存中占用106M的大小,很显然系统是接受不了的。
针对这两种不同的需求我们采用不同的策略。
图片的大小很大,但是需要在android中显示的区域却没有图片真正大小那么大。
首先针对要缩略图这类的需求
压缩图片方式有很多种:
- 质量压缩
- 尺寸压缩
- 采样率压缩
- libjpeg库来进行压缩
一、质量压缩用于本地想服务器上传图片,或者保存到本地的时候
/**
* 质量压缩
* 设置bitmap options属性,降低图片的质量,像素不会减少
* 第一个参数为需要压缩的bitmap图片对象,第二个参数为压缩后图片保存的位置
* 设置options 属性0-100,来实现压缩
*
* @param bmp
* @param file
*/
public static void qualityCompress(Bitmap bmp, File file) {
// 0-100 100为不压缩
int quality = 20;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 把压缩后的数据存放到baos中
bmp.compress(Bitmap.CompressFormat.JPEG, quality, baos);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
二、尺寸压真生实现像素的减少 多用于缓存缩略图
需要考虑将大图片读入内存后的释放事宜。
/**
* 尺寸压缩(通过缩放图片像素来减少图片占用内存大小)
*
* @param bmp
* @param file
*/
public static void sizeCompress(Bitmap bmp, File file) {
// 尺寸压缩倍数,值越大,图片尺寸越小
int ratio = 8;
// 压缩Bitmap到对应尺寸
Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Config.ARGB_8888);
Canvas canvas = new Canvas(result);
Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio);
canvas.drawBitmap(bmp, null, rect, null);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 把压缩后的数据存放到baos中
result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
三、采样率压缩
设置图片的采样率,降低图片像素
不会先将大图片读入内存,大大减少了内存的使用,也不必考虑将大图片读入内存后的释放事宜。不过因为采样率是整数,有时候采样率会不合适。2太大,3太小。
不同的压缩方法,看情景去应用
这就需要BitmapFactory. Options里的两个参数
- inJustDecodeBounds
设置为true就可以让解析方法禁止为bitmap分配内存,就是只会得到里面的数据信息,而不会把bitmap加载到内存里面 - inSampleSize
采样率,这个就是图像数字化的时候,单位时间采样本数,白话就是图片的某一点需要样本去表示。比如我们本来需要16个,采样率变成2 ,那么就变成4个了。
>1 说明高清变普清
<1 就是普清变高清
更加官方的解释请自行百度,我就是比喻一下。
这样我们就可以把图片压缩成我们想要的结果了。
public static int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
// 源图片的高度和宽度
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
// 计算出实际宽高和目标宽高的比率
final int heightRatio = Math.round((float) height / (float) reqHeight);
final int widthRatio = Math.round((float) width / (float) reqWidth);
// 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高
// 一定都会大于等于目标的宽和高。
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 第一次解析将inJustDecodeBounds设置为true,来获取图片大小
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 调用上面定义的方法计算inSampleSize值
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 使用获取到的inSampleSize值再次解析图片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
img.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.drawable.qmsht, 100, 100));
一般来说这样就完了,但是我们的图片如果来自于IO流里面数据呢,这样做就有问题了。
private void loadSLImg() {
InputStream inputStream = null;
try {
inputStream = getAssets().open("qmsht.jpg");
} catch (IOException e) {
e.printStackTrace();
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, options);
options.inSampleSize = calculateInSampleSize(options, 800, 100);
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
img.setImageBitmap(bitmap);
}
结果运行我们发现,并没有出现图片,这是为什么呢?
因为我们之前
options.inJustDecodeBounds = true;
已经生效,所以
Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
bitmap 为null,并不会有位图解析出来。
你可能会说 后来不是 设置 false了吗?
options.inJustDecodeBounds = false;
但是inputStream流文件已经被设置options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, options);方法之后这个流文件某些属性已经被更改。因为源码在Native,所以我们并不是具体情节,所以我们可以做一些测试看看结果。
options.inJustDecodeBounds = false;
InputStream inputStream = null;
try {
inputStream = getAssets().open("qmsht.jpg");
Log.e("inputStream", "原size:" + inputStream.available());
} catch (IOException e) {
e.printStackTrace();
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
Log.e("inputStream", "处理后size:" + inputStream.available());
Log.e("inputStream", "bitmap 是否为null:" + (bitmap==null)+" getWidth "+bitmap.getWidth());
E/inputStream: 原size:8063397
E/inputStream: 处理后size:0
E/inputStream: bitmap 是否为null:false getWidth 30000
说明inputStream被消耗掉了
options.inJustDecodeBounds = true;
InputStream inputStream = null;
try {
inputStream = getAssets().open("qmsht.jpg");
Log.e("inputStream", "原size:" + inputStream.available());
} catch (IOException e) {
e.printStackTrace();
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
Log.e("inputStream", "处理后size:" + inputStream.available());
Log.e("inputStream", "bitmap 是否为null:" + (bitmap==null));
E/inputStream: 原size:8063397
E/inputStream: 处理后size:8057973
E/inputStream: bitmap 是否为null:true
从结果不难看出,所以我们的策略就是,一个流分析,计算出inSampleSize
流一个流来加载图片。
至于根据path来在家图片就没有那么麻烦了
Bitmap bitmap = BitmapFactory.decodeFile(path, options);
你懂的其实他也是两次。
图片的大小很大,需要在Android中可以显示原图大小。
这一类就也很好办,那我就一部分一部分来显示吧,局部显示总不会超吧,如果局部显示也会超,那么先压缩再局部显示。
android 中有这么一个类 BitmapRegionDecoder。看名称就是区域解析。然后通过手势来移动不同的区域,动态的解析不懂得区域,达到看图无缝连接。
private void loadBigImg() {
if (inputStream == null) {
try {
inputStream = getAssets().open("qmsht.jpg");
} catch (IOException e) {
e.printStackTrace();
}
//获得图片的宽、高
BitmapFactory.Options tmpOptions = new BitmapFactory.Options();
tmpOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, tmpOptions);
width = tmpOptions.outWidth;
height = tmpOptions.outHeight;
try {
bitmapRegionDecoder = BitmapRegionDecoder.newInstance(inputStream, false);
} catch (IOException e) {
e.printStackTrace();
}
options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
}
Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(old_x, 0, old_x + width / 40, height), options);
img.setImageBitmap(bitmap);
}
很简单吧,然后可以通过手势,来改变Rect的四个坐标,我们就可以随意看大图了。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
dow_x = (int) event.getX();
break;
case MotionEvent.ACTION_MOVE:
old_x += (int) (dow_x - event.getX());
loadBigImg();
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
结尾
这些都是原理和简单操作,要解决实际问题还需要更细致的处理。共勉~