Bitmap学习(大图片、大长图显示及加载)
前几天公司项目出现一个需求,加载网络大长图,搜索了一些方法,最终是将图片下载到本地,然后通过BitmapRegionDecode.newInstance(...)获取一个对象,然后通过这个对象去调用decodeRegion(mRect, options)得到bitmap,用手势控制图片显示的区域。解决办法的原理就是这样,可是实现起来确实遇到了很多问题,而且晚上也没有很完整的方法,基本都是参照张鸿洋大神的本地加载大图片方法,网络加载有些不太适用,而且我的场景是在recyclerview的item中的imageview。所以走了很多弯路。我这里就顺便复习一下bitmap
#1.Bitmap的概念:
Android中的Bitmap对象是对位图的抽象,它可以从文件系统、资源文件夹、网络等各种不同的来源获取。位图可以看做是像素点的集合,本质上就是通过一系列二进制位来描述一张图片,具有不同色彩格式的位图使用不同数量的二进制位来描述一个像素点,因而图片质量和图片大小也就不同。
获取Bitmap对象并计算它所占用的内存大小:
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.size);
int size = bitmap.getByteCount();
getByteCount()源码:
public final int getByteCount() {
return getRowBytes() * getHeight();
}
getHeight():图片的高度(单位为px), getRowBytes方法返回的是图片的像素宽度与色彩深度的乘积。
所以getByteCount()是这样计算的:像素宽 * 像素高 * 色彩深度,其中色彩深度与Bitmap的色彩格式有关,默认为ARGB_8888.
###### 这里补充一个小知识点吧!
ARGB_4444: 2bytes 每个像素占据2 个字节:A(Alpha)占4位的精度,R(Red)占4位的精度,G(Green)占4位的精度,B(Blue)占4位的精度,加起来一共是16位的精度,折合是2个字节,也就是一个像素占两个字节的内存,同时存储位图的透明度和颜色信息。
ARGB_8888 : 4bytes 每个像素占据4 个字节:这个类型的跟ARGB_4444的原理是一样的,只是A,R,G,B各占8个位的精度,所以一个像素占4个字节的内存。由于该类型的位图质量较好,官方特别推荐使用。但是,如果一个480*800的位图设置了此类型,那个它占用的内存空间是:480*800*4/(1024*1024)=1.5M
RGB_565 : 2bytes 每个像素占据2 个字节:R占5位精度,G占6位精度,B占5位精度,一共是16位精度,折合两个字节。这里注意的时,这个类型存储的只是颜色信息,没有透明度信息
#2.构造Bitmap对象
构造一个类,通常都是通过构造器方法,然而Bitmap是采用了工厂的设计模式,所以一般不会直接调用构造方法。
###1.createBitmap(不建议)
###2.BitmapFactory工厂类的static Bitmap decodeXxx()系
注意,图片的基本加载既不是本文的重点,也不是什么难点,所以这里就不贴详细代码,提示一句,有些方法需要在子线程中
#3.高效的加载Bitmap
在使用bitmap时,经常会遇到内存溢出等情况,这是因为图片太大或者android系统对单个应用施加的内存限制等原因造成的
常见的错误:
OpenGLRenderer: Bitmap too large to be uploaded into a texture
java.lang.OutOfMemoryError:
所以,高效的使用bitmap就显得尤为重要,对他效率的优化也是如此。
系统为我们提供了Options类,通过对它的options进行合理的配置,我们就能够将Bitmap对象调整到令我们满意的大小。
##3.1.Options类介绍
public static class Options {
public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}
...
public Bitmap inBitmap; //用于实现Bitmap的复用,下面会具体介绍
public int inSampleSize; //采样率
public boolean inPremultiplied;
public boolean inDither; //是否开启抖动
public int inDensity; //即上文我们提到的inDensity
public int inTargetDensity; //目标屏幕密度,同上文提到的含义相同
public boolean inScaled; //是否支持缩放
public int outWidth; //图片的原始宽度
public int outHeight; //图片的原始高度
...
}
上面代码是Options类的主要成员变量,我通过对大图片的处理方式去做具体讲解吧!
###1>尺寸压缩(采样率压缩)bitmap.inSampleSize
尺寸压缩是压缩图片的像素,一张图片所占内存的大小 图片类型*宽*高,通过改变三个值减小图片所占的内存,防止OOM,当然这种方式可能会使图片失真 。
第一步,获取图片的原始宽高,通过将Options的inJustDecodeBounds属性设为true后调用decodeXXX方法,BitmapFactory只会解析图片的原始宽高信息,并不会真正的加载图片,请看以下代码:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), resId, options);
//现在原始宽高以存储在了options对象的outWidth和outHeight实例域中
第二步,根据原始宽高计算出inSampleSize,代码如下:
//dstWidth和dstHeight分别为目标ImageView的宽高
public static int calSampleSize(BitmapFactory.Options options, int dstWidth, int dstHeight) {
int rawWidth = options.outWidth;
int rawHeight = options.outHeight;
int inSampleSize = 1;
if (rawWidth > dstWidth || rawHeight > dstHeight) {
float ratioHeight = (float) rawHeight / dstHeight;
float ratioWidth = (float) rawWidth / dstHeight;
inSampleSize = (int) Math.min(ratioWidth, ratioHeight);
}
return inSampleSize;
}
第三步,将BitmapFacpry.Options的inSampleSize参数设为false并重新加载图片
options.inJustDecodeBounds =false;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);
这样就可以实现图片尺寸的压缩
###2>质量压缩bitmap.compress
质量压缩是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,经过它压缩的图片文件大小(kb)会有改变,但是导入成bitmap后占得内存是不变的,宽高也不会改变。
ByteArrayOutputStream baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
int options = 90;
int bytes = baos.toByteArray().length;
while ((bytes / 1024 > 500) && (options >= 20)) { //循环判断如果压缩后图片是否大于500kb,大于继续压缩
baos.reset();//重置baos即清空baos
options -= 10;//每次都减少10
//第一个参数 :图片格式 ,第二个参数: 图片质量,100为最高,0为最差 ,第三个参数:保存压缩后的数据的流
image.compress(Bitmap.CompressFormat.JPEG, options, baos);//这里压缩options%,把压缩后的数据存放到baos中
bytes = baos.toByteArray().length;
}
image.recycle();
可以看到,图片的大小是没有变的,因为质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,这也是为什么该方法叫质量压缩方法。那么,图片的长,宽,像素都不变,那么bitmap所占内存大小是不会变的。
Bitmap.compress方法确实可以压缩图片,但压缩的是存储大小,即你放到disk上的大小
###3>缩放法压缩 martix
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
bm = Bitmap.createBitmap(bit, 0, 0, bit.getWidth(),
bit.getHeight(), matrix, true);
Log.i("wechat", "压缩后图片的大小" + (bm.getByteCount() / 1024 / 1024)
+ "M宽度为" + bm.getWidth() + "高度为" + bm.getHeight());
以上三种方法适用场景:大图片上传,一般是1,2方法结合使用,网上有很多最优算法,但是实现思路不变,下面再说说大图片显示问题
##3.2.BitmapRegionDecoder类介绍
BitmapRegionDecoder类用来编译(解码)在图片内不同的方形区域,BitmapRegionDecoder类在使用较大图片只需要取得图片中的一小部分的内容是特别有效益的。
我们创建一个BitmapRegionDecoder类,并调用newInstace()方法,就可以得到BitmapRegionDecoder的对象之后,我们就能调用decodeRegion()方法去多次获得位图的特定地区的小图片
###1>BitmapRegionDecoder的创建
#####public static BitmapRegionDecoder newInstance (String pathName, boolean isShareable)
用于创建BitmapRegionDecoder,pathName表示路径,只有jpeg和png图片才支持这种方式,isShareable如果为true,那BitmapRegionDecoder会对输入流保持一个表面的引用,如果为false,那么它将会创建一个输入流的复制,并且一直使用它。即使为true,程序也有可能会创建一个输入流的深度复制。如果图片是逐步解码的,那么为true会降低图片的解码速度。如果路径下的图片不是支持的格式,那就会抛出异常
#####public static BitmapRegionDecoder newInstance (InputStream is, boolean isShareable)
同上,不过参数is变成了输入流
#####public static BitmapRegionDecoder newInstance (FileDescriptor fd, boolean isShareable)
同上,不过参数变成了文件描述符,在函数运行完之前,文件描述符不可以改变
#####public static BitmapRegionDecoder newInstance (byte[] data, int offset, int length, boolean isShareable)
同上,不过参数变成了字节数组,offset为起始位置,length为长度
###2>BitmapRegionDecoder的解码区域
#####public Bitmap decodeRegion (Rect rect, BitmapFactory.Options options)
参数一很明显是一个rect,参数二是BitmapFactory.Options,可以控制图片的inSampleSize,inPreferredConfig等。
本地加载大图片,我之前分享过一篇文章:
#####https://www.jianshu.com/p/5a250b68c331
网络加载大图片,思路差不多,但是需要先下载到本地再进行一系列操作,具体代码,就不展示了,可以提供个github库,大家看一看就应该懂了:
#####compile 'com.shizhefei:LargeImageView:1.0.9'
因为是公司项目,所以暂时不方便展示具体代码,后期抽时间提炼出来个demo贴出来。。。
到这里其实就差不多了。。。
谢谢阅读,我这里也是学习的态度在这里分享,有什么问题希望大家能提出来能提出来,随时可以和我交流探讨:QQ:707086125 微信:loveme_dp
#1.Bitmap的概念:
Android中的Bitmap对象是对位图的抽象,它可以从文件系统、资源文件夹、网络等各种不同的来源获取。位图可以看做是像素点的集合,本质上就是通过一系列二进制位来描述一张图片,具有不同色彩格式的位图使用不同数量的二进制位来描述一个像素点,因而图片质量和图片大小也就不同。
获取Bitmap对象并计算它所占用的内存大小:
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.size);
int size = bitmap.getByteCount();
getByteCount()源码:
public final int getByteCount() {
return getRowBytes() * getHeight();
}
getHeight():图片的高度(单位为px), getRowBytes方法返回的是图片的像素宽度与色彩深度的乘积。
所以getByteCount()是这样计算的:像素宽 * 像素高 * 色彩深度,其中色彩深度与Bitmap的色彩格式有关,默认为ARGB_8888.
###### 这里补充一个小知识点吧!
ARGB_4444: 2bytes 每个像素占据2 个字节:A(Alpha)占4位的精度,R(Red)占4位的精度,G(Green)占4位的精度,B(Blue)占4位的精度,加起来一共是16位的精度,折合是2个字节,也就是一个像素占两个字节的内存,同时存储位图的透明度和颜色信息。
ARGB_8888 : 4bytes 每个像素占据4 个字节:这个类型的跟ARGB_4444的原理是一样的,只是A,R,G,B各占8个位的精度,所以一个像素占4个字节的内存。由于该类型的位图质量较好,官方特别推荐使用。但是,如果一个480*800的位图设置了此类型,那个它占用的内存空间是:480*800*4/(1024*1024)=1.5M
RGB_565 : 2bytes 每个像素占据2 个字节:R占5位精度,G占6位精度,B占5位精度,一共是16位精度,折合两个字节。这里注意的时,这个类型存储的只是颜色信息,没有透明度信息
#2.构造Bitmap对象
构造一个类,通常都是通过构造器方法,然而Bitmap是采用了工厂的设计模式,所以一般不会直接调用构造方法。
###1.createBitmap(不建议)
###2.BitmapFactory工厂类的static Bitmap decodeXxx()系
注意,图片的基本加载既不是本文的重点,也不是什么难点,所以这里就不贴详细代码,提示一句,有些方法需要在子线程中
#3.高效的加载Bitmap
在使用bitmap时,经常会遇到内存溢出等情况,这是因为图片太大或者android系统对单个应用施加的内存限制等原因造成的
常见的错误:
OpenGLRenderer: Bitmap too large to be uploaded into a texture
java.lang.OutOfMemoryError:
所以,高效的使用bitmap就显得尤为重要,对他效率的优化也是如此。
系统为我们提供了Options类,通过对它的options进行合理的配置,我们就能够将Bitmap对象调整到令我们满意的大小。
##3.1.Options类介绍
public static class Options {
public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}
...
public Bitmap inBitmap; //用于实现Bitmap的复用,下面会具体介绍
public int inSampleSize; //采样率
public boolean inPremultiplied;
public boolean inDither; //是否开启抖动
public int inDensity; //即上文我们提到的inDensity
public int inTargetDensity; //目标屏幕密度,同上文提到的含义相同
public boolean inScaled; //是否支持缩放
public int outWidth; //图片的原始宽度
public int outHeight; //图片的原始高度
...
}
上面代码是Options类的主要成员变量,我通过对大图片的处理方式去做具体讲解吧!
###1>尺寸压缩(采样率压缩)bitmap.inSampleSize
尺寸压缩是压缩图片的像素,一张图片所占内存的大小 图片类型*宽*高,通过改变三个值减小图片所占的内存,防止OOM,当然这种方式可能会使图片失真 。
第一步,获取图片的原始宽高,通过将Options的inJustDecodeBounds属性设为true后调用decodeXXX方法,BitmapFactory只会解析图片的原始宽高信息,并不会真正的加载图片,请看以下代码:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), resId, options);
//现在原始宽高以存储在了options对象的outWidth和outHeight实例域中
第二步,根据原始宽高计算出inSampleSize,代码如下:
//dstWidth和dstHeight分别为目标ImageView的宽高
public static int calSampleSize(BitmapFactory.Options options, int dstWidth, int dstHeight) {
int rawWidth = options.outWidth;
int rawHeight = options.outHeight;
int inSampleSize = 1;
if (rawWidth > dstWidth || rawHeight > dstHeight) {
float ratioHeight = (float) rawHeight / dstHeight;
float ratioWidth = (float) rawWidth / dstHeight;
inSampleSize = (int) Math.min(ratioWidth, ratioHeight);
}
return inSampleSize;
}
第三步,将BitmapFacpry.Options的inSampleSize参数设为false并重新加载图片
options.inJustDecodeBounds =false;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId, options);
这样就可以实现图片尺寸的压缩
###2>质量压缩bitmap.compress
质量压缩是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,经过它压缩的图片文件大小(kb)会有改变,但是导入成bitmap后占得内存是不变的,宽高也不会改变。
ByteArrayOutputStream baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
int options = 90;
int bytes = baos.toByteArray().length;
while ((bytes / 1024 > 500) && (options >= 20)) { //循环判断如果压缩后图片是否大于500kb,大于继续压缩
baos.reset();//重置baos即清空baos
options -= 10;//每次都减少10
//第一个参数 :图片格式 ,第二个参数: 图片质量,100为最高,0为最差 ,第三个参数:保存压缩后的数据的流
image.compress(Bitmap.CompressFormat.JPEG, options, baos);//这里压缩options%,把压缩后的数据存放到baos中
bytes = baos.toByteArray().length;
}
image.recycle();
可以看到,图片的大小是没有变的,因为质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,这也是为什么该方法叫质量压缩方法。那么,图片的长,宽,像素都不变,那么bitmap所占内存大小是不会变的。
Bitmap.compress方法确实可以压缩图片,但压缩的是存储大小,即你放到disk上的大小
###3>缩放法压缩 martix
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
bm = Bitmap.createBitmap(bit, 0, 0, bit.getWidth(),
bit.getHeight(), matrix, true);
Log.i("wechat", "压缩后图片的大小" + (bm.getByteCount() / 1024 / 1024)
+ "M宽度为" + bm.getWidth() + "高度为" + bm.getHeight());
以上三种方法适用场景:大图片上传,一般是1,2方法结合使用,网上有很多最优算法,但是实现思路不变,下面再说说大图片显示问题
##3.2.BitmapRegionDecoder类介绍
BitmapRegionDecoder类用来编译(解码)在图片内不同的方形区域,BitmapRegionDecoder类在使用较大图片只需要取得图片中的一小部分的内容是特别有效益的。
我们创建一个BitmapRegionDecoder类,并调用newInstace()方法,就可以得到BitmapRegionDecoder的对象之后,我们就能调用decodeRegion()方法去多次获得位图的特定地区的小图片
###1>BitmapRegionDecoder的创建
#####public static BitmapRegionDecoder newInstance (String pathName, boolean isShareable)
用于创建BitmapRegionDecoder,pathName表示路径,只有jpeg和png图片才支持这种方式,isShareable如果为true,那BitmapRegionDecoder会对输入流保持一个表面的引用,如果为false,那么它将会创建一个输入流的复制,并且一直使用它。即使为true,程序也有可能会创建一个输入流的深度复制。如果图片是逐步解码的,那么为true会降低图片的解码速度。如果路径下的图片不是支持的格式,那就会抛出异常
#####public static BitmapRegionDecoder newInstance (InputStream is, boolean isShareable)
同上,不过参数is变成了输入流
#####public static BitmapRegionDecoder newInstance (FileDescriptor fd, boolean isShareable)
同上,不过参数变成了文件描述符,在函数运行完之前,文件描述符不可以改变
#####public static BitmapRegionDecoder newInstance (byte[] data, int offset, int length, boolean isShareable)
同上,不过参数变成了字节数组,offset为起始位置,length为长度
###2>BitmapRegionDecoder的解码区域
#####public Bitmap decodeRegion (Rect rect, BitmapFactory.Options options)
参数一很明显是一个rect,参数二是BitmapFactory.Options,可以控制图片的inSampleSize,inPreferredConfig等。
本地加载大图片,我之前分享过一篇文章:
#####https://www.jianshu.com/p/5a250b68c331
网络加载大图片,思路差不多,但是需要先下载到本地再进行一系列操作,具体代码,就不展示了,可以提供个github库,大家看一看就应该懂了:
#####compile 'com.shizhefei:LargeImageView:1.0.9'
因为是公司项目,所以暂时不方便展示具体代码,后期抽时间提炼出来个demo贴出来。。。
到这里其实就差不多了。。。
谢谢阅读,我这里也是学习的态度在这里分享,有什么问题希望大家能提出来能提出来,随时可以和我交流探讨:QQ:707086125 微信:loveme_dp