图像与图形处理
Canvas中就保存着一个Bitmap对象,调用canvas的各种绘图函数,最终还是绘制到其中的Bitmap上。我们自定义View是,一般都会重写onDraw(Canvas canvas),这个函数中是自带Canvas的,只需要将画的内容调用Canvas的函数画出来,就会直接显示在对应的View上。其实,View对应着一个Bitmap,而onDraw()中canvas就是通过这个Bitmap创建出来的。
Android中:图片除了.png、.jpg等格式位图,还用资源文件中定义的Drawable对象。
1、Bitmap
bitmap被称为位图,一般位图的文件格式扩展名为.bmp,作为一种逐像素显示的对象,由一个个像素点组成;因为由一个个像素组成,肯定涉及到两个问题:
- 如何存储每个像素
- 相关的像素点之间能否压缩
1.1、Bitmap在绘图中使用
-
转换为BitmapDrawable对象使用
BitmapDrawable drawable=new BitmapDrawable(bitmap); iv.setImageDrawable(drawable); -
当做画布使用
方式一:使用默认画布
onDraw(Canvas canvas)和dispatchDraw(Canvas canvas)中的Canvas对应这一个Bitmap对象,所有的绘图都是显示这个Bitmap上的,这个Bitmap就是默认画布;
方式二:自建画布
有是我们需要在特定的Bitmap上作画或者只需要一块空白的画布;需要我们自己来创建Canvas对象;
Bitmap bitmap=Bitmap.createBitmap(200,100,Bitmap.Config.ARGB_8888);
Canvas canvas=new Canvas(bitmap);
canvas.drawColor(Color.RED)
1.2、存储每个像素点
一张位图所占的内存=图片长度(px)* 图片宽度 * 一个像素点所占的字节数;
在Android中存储一个 像素所使用的字节数使用枚举类型Bitmap.Config中的各个参数来表示的:
- ALPHA_8:表示每个像素只存储8位透明度值,不存储颜色值,即每个像素占一个字节;
- ARGB_4444:表示16位ARGB位图,即A、R、G、B各占4位,每一个像素点占2个字节;
- ARGB_8888:表示32位ARGB位图,即A、R、G、B各占8位,每一个像素点占4个字节;
- RGB_565:表示16位RGB位图,即R占5位,G占6位,B占5位,每一像素点占2个字节;
1.3、bitmap压缩格式
在Android中,压缩格式使用枚举类Bitmap.CompressFormat的成员变量表示JPEG、PNG、WEBP;
第一部分 图片加载
bitmap代表一个位图,BitmapDrawable中封装的图片就是一个bitmap
BitmapDrawble bd=new BitmapDrawable(bitmap);
Bitmap静态创建bitmap对象
***creatBitmap(Bitmap source,int x,int y,int width,int height)😗**从指定位图指定坐标(x,y)处截取指定宽高的大小,创建新Bitmap
***creatScaleBitmap(Bitmap src,int dstWidth,int dstHeight,boolean filter)😗**对原位图src进行缩放,缩成dstWidth、dstHeight的新位图。
***creatBitmap(int width,int height,Bitmap.config config)😗创建一个指定宽高的新位图
***creatBitmap(Bitmap source,int x,int y,int width,int height,Matrix m,boolean filter)😗**从源位图的指定坐标开始,截取指定宽高的形状,按Matrix指定规则变换创建新的Bitmap
1.2、BitmapFacotry加载Bitmap
作用:从不同数据源中解析、创建Bitmap对象
***decodeByteArray(byte[] data,int offset,int length):***从指定字节数组的offset位置开始,将指定长度的字节数据解析成bitmap对象
decodeFile(String path):从指定文件中解析创建bitmap对象,path必须为全路径名;
decodeFileDescriptor(FileDescriptor fd):从指定FileDescriptor对应的文件中解析创建bitmap对象
decodeResource(Resource res,int id):从给定资源ID中解析、创建bitmap对象;主要以R.drawable.xxx从本地资源中加载;
decodeStream(InputStream is):从指定输入流中解析、创建bitmap对象
注意:手机内存较小,不停的解析、创建bitmap对象,可能由于之前bitmap对象占用的内存还未回收,导致出现内存溢异常。
对于decodeFile()函数,补充一点:关于android存储系统根目录下一些常用的文件夹:
- cache:缓冲区目录,用于存放临时文件
- data:主要存放数据文件,其下的子目录用于存放APP相关分类数据;其中data/app目录下存放的是用户安装的APK文件;data/data/存放的是系统中所有APP的数据文件,以APK包名区分,其中会有提交的数据库和XML数据文件;
- sdcard:SD卡的挂载点,其下的子目录用于存放SD卡上的文件;
- system:这是android中最重要的文件目录,主要存放系统文件;
- tmp:存放临时文件;
1.3、Bitmapfactory.Options
通过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 ColorSpace inPreferredColorSpace = null;
public boolean inPremultiplied;
public boolean inDither;
public int inDensity;
public int inTargetDensity;
public int inScreenDensity;
public boolean inScaled;
@Deprecated
public boolean inInputShareable;
public boolean inPreferQualityOverSpeed;
public int outWidth;
public int outHeight;
public String outMimeType;
public Bitmap.Config outConfig;
public ColorSpace outColorSpace;
public byte[] inTempStorage;
public boolean mCancel;
}
以in开头成员变量表示设置相应的某某参数,以out开头的就表示获取某某参数;比如:inSampleSize:设置采样率;outWidth:表示获取图片原始宽度
- inSampleSize:采样率(缩放比,比如1:表示原图,2表示宽高均缩小原来的1/2),官方文档支出,inSampleSize总是为2的指数即2、4、8、16.。。,如果不为2的指数,回向下去最接近2的指数.s是指每隔多少个像素采样一次作为结果;比如inSampleSize=4:表示每四个采样样本中取一个像素作为返回结果,其余的被丢弃;这里以实例讲解:
假如一个ImageView为100×100px,要显示的图片为300×400px,图片宽度为控件的三倍,高度为控件的四倍;如果将图片缩放四倍,那么图片的大小就位75×100px,宽度将小于控件宽度,显示宽度将会需要进行拉伸;本来压缩就已经就是一种失真压缩了,如果这儿在拉伸,就更加失真了。所以采用率应该取宽高缩放的最小值;
- injustDecodeBounds:为true:表示只会去解析原始图片宽高,并不会去真正加载图片。通过原始图片宽高以及需求宽高即可算出缩放比,再将其设为false去加载缩放后的图片。
- inScaled:默认为true,如果设为false,当屏幕分辨率于图片所在资源文件夹对应的分辨率不同时,默认情况是需要动态缩放的,这时不缩放
- inDensity:用于设置文件所在资源文件夹的屏幕分辨率
- inTargetDensity:表示真实显示的屏幕分辨率
- inPreferredConfig:用来设置像素的存储格式
通过缩放加载图片流程:
Android提供两个方法来判断bitmap对象是否回收,以及强制回收:
- boolean isRecycled():返回该bitmap是否回收
- void recycle():强制该bitmap对象立即回收
1.4、加载一个Bitmap文件需要占用多少空间
前面我们说了bitmap大小计算方法=图片高度×图片宽度×每个像素所占字节数;当我从资源文件中或者本地加载bitmap需要多少内存?android系统在加载bitmap时会根据需要动态缩放这张图片所占像素;android手机屏幕尺寸错综复杂,资源目录下有不同的drawable-XXdpi文件夹来适配不同的屏幕分辨率;但是android是可以定制的,注定它会有不止这些屏幕分辨率,当遇到和这些文件夹不同分辨率的屏幕时该怎么办:显然需要动态缩放图片了,缩放比=屏幕分辨率/文件所在文件夹分辨率,这儿我们还是以实例讲解:Drawable—xhdpi对应的分辨率是480dpi,而真实的屏幕分辨率是720dpi,显然就需要放大图片,已适配屏幕,放大倍数为=720/480=1.5.也就是说xhdpi文件夹下原本一张100×200px,在显示加载内存时会被放大1.5倍,实际生成的bitmap尺寸是150×300px.
另外,这里还有一个问题可能比较好奇:图片如何被放大的?
宽度方向上平白多出50个像素,高度方向上多出100个像素。这些多出的像素是如何被填充的?这就涉及到图片填充算法:将原来的像素平铺,多出来的空白像素用相邻两个颜色的中间值填充过度;
当我们中SD等文件中加载图片,
File file=Environment.getExternalStorageDirectory();
String path=file.getAbsolutePath+"/leslie.png";
Bitmap bitmap=BitmapFactory.decodeFile(path);
图片不会缩放,占用内存为=图片高度像素×图片宽度像素×每个像素占用的字节数;
结论:
- 不同名称的资源文件夹是为了适配不同的屏幕分辩率,当屏幕分辨率与文件所在文件夹对应的分辩率不同时进行缩放=屏幕分辨率/文件夹对应分辨率
- 从本地文件中加载图片不会对图片进行缩放;
1.5、Bitmap像素是否可以改变的问题
其函数:copy(Config config,boolean isMutable)
根据源图像创建一个副本,但可以指定副本的像素存储格式,以及副本是否可以更改其中的像素;
Boolean isMutable()函数用于判断当前的bitmap是不是像素是可更改的;如果不可更改,仍然利用setPiexl()等设置其中的像素值,将会异常;
那可能你会问,哪些方法加载的bitmap是像素可以改变的,那些方法加载的bitmap是像素不可以改变的??
答案:凡是通过BitmapFactory加载的bitmap都是像素不可变的,只有通过Bitmap创建的其中几个函数生成的Bitmap是像素可以更改的;
createBitmap(int width,int height,Bitmap.Config config)
copy(Bitmap.Config config,boolean isMutable)
createScaledBitmap(Bitmap src,int dstWidth,int dsHeight,boolean filter)
createBitmap(DisplayMetrcs display,int width,int height,Bitmap.Congfig config)
注意:createScaledBitmap()函数创建缩放Bitmap时,如果源图像和目标图像宽高一致时,将不会生成新的图像,而是直接返回源图像,此时如果源图像是不可更改的,那么返回的图像就不可更改;只有进行实际的缩放(源图像与目标图像宽高不一致)才会生成新的图像,而新生成的图像才是像素看更改的;
***对不可更改像素的图像,是不可以作为画布的;***比如:
Bitmap bitmap=BitmapFactory.decodeResource(getResource(),R.drawable.dog);
Canvs canvas=new Canvs(bitmap);
canvas.drawColor(Color.RED)
这里由于bitmap是像素不可更改的,当其作为Canvas以后,如果要向其填充颜色,比如就会改变Bitmap的像素值,自然就会报错;
1.5、bitmap创建函数
1.5.1、获取Bitmap所分配的内存大小
-
方式一:API 1引入
getRowBytes():获取每行所分配的内存 如果要获取整个Bitmap分配的内存 Bitmap所占的内存=getRowBytes()*bitmap.getHeight(); -
方式二:API 12引入
int getByteCount() -
. 方式三:API 19引入
int getAllocationByCount
所以一般在计算Bitmap内存占用时,最完善的写法就是:
public int getBitmapSize(Bitmap bitmap{
if(Build.VERSION.SDK_INT>Build.VERSION_CODES.KITKAT){
return bitmap.getAllocationByteCount();
}else if(Build.VERSION.SDK_INT>=12){
return bitmap.getByteCount();
}else{
return bitmap.getRowBytes()*bitmap.getHeight();
}
}
1.5.2、回收Bitmap所占内存
Boolean isRecycled():判断Bitmap所占内存是否被回收
recyle():回收内存
注意:如果bitmap内存被回收,再次使用该bitmap将会报错;
if(bm!=null&& !bm.isRecyled()){
bm.recyle();
bm=null;
System.gc();
}
在API 10(Android 2.3.3)之前,bitmap的像素即数据被放在Native内存空间之中,这些数据本身与Bitmap是隔离的,Bitmap本身放在Dalvik堆中,无法预测native内存何时释放像素级数据所占内存,这就容易造成他的内存超过限制而崩溃;API 10以上,像素级数据和Bitmap本身一起被放在Dalvik堆中,可以通过Java回收机制回收;
1.5.3、compress()压缩图像
将压缩过的Bitmap写入指定的输出流中;
public Boolean compress(CompressFormat format,int quality,OutputStream stream)
format:图像压缩格式:CompressFormat.JPEG、CompressFormat.PNG、CompressFormat.WEBP
int quality:压缩后的图像画质,取值为0~100,值越大压缩后画质越低;如果是无损压缩格式,该参数将被忽略
stream:bitmap压缩后,会以OutputStream的形式在这里输出;
三种压缩格式:
JPEG:是一种有损压缩格式,压缩过程中会改变图像的原本质量,quality值越小,画质越差,对图片的画质损害越大,得到的图像文件大小越小;JPEG不支持ALPHA透明度,遇到透明度像素是,会以黑色填充;
PNG:支持透明度的无损压缩格式;
WEBP:同时支持无损和有损压缩格式;API14~API 17,只支持有损压缩格式,而且不支持透明度;API 18以后,webp是一种无损压缩格式,而且支持透明度;
Bitmap、Drawable、Canvas、View四者关系
Bitmap与Canvas
构造Canvas的两种方式:
Canvas canvas=new Canvas(bitmap);
或者
Canvas canvas=new Canvas();
canvas.setBimap(bitmap);
Canvas中的画布实际上就是Bitmap,调用Canvas的各个绘图函数实际上都是绘制在bitmap上的;
Bitmap与View关系
在自定义控件时,如果该控件派生自View,则会重写onDraw(Canvas canvas),而调用canvas的绘图函数之后,会直接表现在View上。显示的View也是通过Canvas中的Bitmap来显示的;
public void draw(Canvas canvas){
......
if(!dirtyOpaque)
onDraw(canvas);
}
继续找draw()中的canvas来自于:
public Bitmap createSnapshot(Bitmap.Config quality, int backgroundColor, boolean skipChildren) {
.....
Bitmap bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(),
width > 0 ? width : 1, height > 0 ? height : 1, quality);
Canvas canvas;
if (attachInfo != null) {
canvas = attachInfo.mCanvas;
if (canvas == null) {
canvas = new Canvas();
}
canvas.setBitmap(bitmap);
// Temporarily clobber the cached Canvas in case one of our children
// is also using a drawing cache. Without this, the children would
// steal the canvas by attaching their own bitmap to it and bad, bad
// things would happen (invisible views, corrupted drawings, etc.)
attachInfo.mCanvas = null;
} else {
// This case should hopefully never or seldom happen
canvas = new Canvas(bitmap);
}
....
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
dispatchDraw(canvas);
}else{
draw(canvas);
}
}
View显示的内容是通过绘制一个内置的bitmap来呈现的;
Bitmap与Drawable
我们在自定义Drawable时,要想将Drawable显示在View上,必须使用类似下面的的方法:
public void onDraw(Canvas canvas){
ShapeDrawable drawable=new ShapeDrawable(new RectShape())
drawable.setBounds(50,50,200,100);
drawable.draw(canvas);
}
这里的canvas即View中的Canvas,我们看Drawable的draw()由于该方法为抽象方法,我们以ShapeDrawable为例,看看其draw():
public void draw(Canvas canvas) {
.....
if (state.mShape != null) {
// need the save both for the translate, and for the (unknown)
// Shape
final int count = canvas.save();
canvas.translate(r.left, r.top);
onDraw(state.mShape, canvas, paint);
canvas.restoreToCount(count);
} else {
canvas.drawRect(r, paint);
}
}
内部调用了onDraw()完成相应的绘制;
protected void onDraw(Shape shape, Canvas canvas, Paint paint) {
shape.draw(canvas, paint);
}
这里的shape即我们传入的RectShape:在进入其draw():
@Override
public void draw(Canvas canvas, Paint paint) {
canvas.drawRect(mRect, paint);
}
可以看见其就是利用canvas绘制这个矩形;
常见问题:
- 现在Bitmap上绘制,在将bitmap绘制到Canvas上;
如果我们要保存这张被绘制的图形或者需要绘制透明像素,则可以先将图形绘制到Bitmap上,在将Bitmap绘制到canvas;比如我们在自定义View是要会在新建的画布绘制一个矩形;
Canvas canvas=new Canvas(Bitmap.create(....));
canvas.drawRect()
这时只是将矩形绘制到了这张新的Bitmap上,如果你要想将该bitmap在View上显示,还要调用自定义View对应的canvas.drawBitmap()绘制这个bitmap;
public void draw(Canvas canvas, Paint paint) {
canvas.drawRect(mRect, paint);
}
可以看见其就是利用canvas绘制这个矩形;
常见问题:
- 现在Bitmap上绘制,在将bitmap绘制到Canvas上;
如果我们要保存这张被绘制的图形或者需要绘制透明像素,则可以先将图形绘制到Bitmap上,在将Bitmap绘制到canvas;比如我们在自定义View是要会在新建的画布绘制一个矩形;
Canvas canvas=new Canvas(Bitmap.create(....));
canvas.drawRect()
这时只是将矩形绘制到了这张新的Bitmap上,如果你要想将该bitmap在View上显示,还要调用自定义View对应的canvas.drawBitmap()绘制这个bitmap;
来源:CSDN
作者:Linleslie
链接:https://blog.csdn.net/Leslie_LN/article/details/103736851