绘制系列(十八)图形篇-Bitmap

冷暖自知 提交于 2019-12-28 04:17:11

图像与图形处理

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. 如何存储每个像素
  2. 相关的像素点之间能否压缩

1.1、Bitmap在绘图中使用

  1. 转换为BitmapDrawable对象使用

     	BitmapDrawable drawable=new BitmapDrawable(bitmap);
     	iv.setImageDrawable(drawable);
    
  2. 当做画布使用

方式一:使用默认画布

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中的各个参数来表示的:

  1. ALPHA_8:表示每个像素只存储8位透明度值,不存储颜色值,即每个像素占一个字节;
  2. ARGB_4444:表示16位ARGB位图,即A、R、G、B各占4位,每一个像素点占2个字节;
  3. ARGB_8888:表示32位ARGB位图,即A、R、G、B各占8位,每一个像素点占4个字节;
  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存储系统根目录下一些常用的文件夹:

  1. cache:缓冲区目录,用于存放临时文件
  2. data:主要存放数据文件,其下的子目录用于存放APP相关分类数据;其中data/app目录下存放的是用户安装的APK文件;data/data/存放的是系统中所有APP的数据文件,以APK包名区分,其中会有提交的数据库和XML数据文件;
  3. sdcard:SD卡的挂载点,其下的子目录用于存放SD卡上的文件;
  4. system:这是android中最重要的文件目录,主要存放系统文件;
  5. 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:表示获取图片原始宽度

  1. inSampleSize:采样率(缩放比,比如1:表示原图,2表示宽高均缩小原来的1/2),官方文档支出,inSampleSize总是为2的指数即2、4、8、16.。。,如果不为2的指数,回向下去最接近2的指数.s是指每隔多少个像素采样一次作为结果;比如inSampleSize=4:表示每四个采样样本中取一个像素作为返回结果,其余的被丢弃;这里以实例讲解:

假如一个ImageView为100×100px,要显示的图片为300×400px,图片宽度为控件的三倍,高度为控件的四倍;如果将图片缩放四倍,那么图片的大小就位75×100px,宽度将小于控件宽度,显示宽度将会需要进行拉伸;本来压缩就已经就是一种失真压缩了,如果这儿在拉伸,就更加失真了。所以采用率应该取宽高缩放的最小值;

  1. injustDecodeBounds:为true:表示只会去解析原始图片宽高,并不会去真正加载图片。通过原始图片宽高以及需求宽高即可算出缩放比,再将其设为false去加载缩放后的图片。
  2. inScaled:默认为true,如果设为false,当屏幕分辨率于图片所在资源文件夹对应的分辨率不同时,默认情况是需要动态缩放的,这时不缩放
  3. inDensity:用于设置文件所在资源文件夹的屏幕分辨率
  4. inTargetDensity:表示真实显示的屏幕分辨率
  5. inPreferredConfig:用来设置像素的存储格式

通过缩放加载图片流程:

35

Android提供两个方法来判断bitmap对象是否回收,以及强制回收:

  1. boolean isRecycled():返回该bitmap是否回收
  2. 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. 不同名称的资源文件夹是为了适配不同的屏幕分辩率,当屏幕分辨率与文件所在文件夹对应的分辩率不同时进行缩放=屏幕分辨率/文件夹对应分辨率
  2. 从本地文件中加载图片不会对图片进行缩放;

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绘制这个矩形;

常见问题:

  1. 现在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绘制这个矩形;

常见问题:

  1. 现在Bitmap上绘制,在将bitmap绘制到Canvas上;

如果我们要保存这张被绘制的图形或者需要绘制透明像素,则可以先将图形绘制到Bitmap上,在将Bitmap绘制到canvas;比如我们在自定义View是要会在新建的画布绘制一个矩形;

		Canvas canvas=new Canvas(Bitmap.create(....));

		canvas.drawRect()

这时只是将矩形绘制到了这张新的Bitmap上,如果你要想将该bitmap在View上显示,还要调用自定义View对应的canvas.drawBitmap()绘制这个bitmap;

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!