Bitmap的基础用法

1、前言

在前面的两讲当中,我们研究了Drawable是什么东西,我们今天一块来探讨一下什么是Bitmap,首先我们要明白,Drawable是Android当中所有可以绘制事物的一个抽象的概念,而Bitmap是可以绘制的,所以Bitmap属于Drawable的一种,所以Drawable是大的范围,而Bitmap是小的范围,如果是这样的话,就会衍生出,一个Bitmap是Drawable,而Drawable不一定是Bitmap。这也是两者的区别所在,我们在前两讲当中提过对Bitmap进行的一个封装就是BitmapDrawable而BitmapDrawable是Drawable的一个子类,想想我们当时定义的Xml文件,我相信大家会理解这两者的区别。

2、正文

今天的内容我先给大家看一幅图片,大家应该就明白了

Bitmap知识导航

今天的重点是关于Bitmap,至于关于如何绘制,以后会讲解的,下面我们就上述三点来开始今天的博客,不过在开始之前我们一定到明白,Drawable和Bitmap的区别。

Bitmap是一张位图,扩展名为.bmp.dip,为什么说它是位图呢,因为它将图像定义为由点(像素)组成,每个点可以由多种色彩表示,包括2、4、8、16、24和32位色彩,例如一幅1024×768分辨率的32位真彩图片,其所占存储字节数为:1024×768×32/8=3072KB。位图文件图像效果好,但是非压缩格式的,需要占用较大存储空间,所以我们遇到的OOM大部分都是由Bitmap的造成的

如果还是不太清楚Bitmap和Drawable的区别的话,那我们现在就理解为,Drawable实际上就是一个空的架子,里面可以以装载各式各样的图像,而bitmap就是其中的一幅图像,这种图像图像质量比较好。而前者重行为,而后者重数据(图像数据)像一般的移动架子之类的我们就会对其封装到Drawable当中,而如果我们要去改动画本身的属性,就去修改Bitmap,或者其他的画。

3、Bitmap怎么来的(怎么创建一个Bitmap,或者通过资源生成一个Bitmap)

Bitmap是不能New出来的,因为它的构造器私有,并且它的实例化是JNI做的,所以只有去实例化了JNI我们才能去创建Bitmap,既然是JNI做的,那么必然Google会给我提供使用的方法,不错那就Bitmap.createBitmap();通过这个方法我们就能得到一个Bitmap。

在我们创建Bitmap的时候,涉及到一个名为Bitmap.Config的这个类,这个类的具体的作用就是在我们构建自己新的Bitmap的时候来告诉系统,构建一个什么质量的Bitmap。好的 还是坏的,点进去我们看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public enum Config {
ALPHA_8 (1),
RGB_565 (3),
@Deprecated
ARGB_4444 (4),
ARGB_8888 (5),
final int nativeInt;

private static Config sConfigs[] = {
null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888
};

Config(int ni) {
this.nativeInt = ni;
}

static Config nativeToConfig(int ni) {
return sConfigs[ni];
}
}
public enum Config {
ALPHA_8 (1),
RGB_565 (3),
@Deprecated
ARGB_4444 (4),
ARGB_8888 (5),
final int nativeInt;

private static Config sConfigs[] = {
null, ALPHA_8, null, RGB_565, ARGB_4444, ARGB_8888
};

Config(int ni) {
this.nativeInt = ni;
}

static Config nativeToConfig(int ni) {
return sConfigs[ni];
}
}

大家也看到了在这其中主要维护了4个变量,来表示图片质量的高低,分别是:

  • ALPHA_8 Alpha:由8位组成
  • ARGB_4444:由4个4位组成即16位,
  • ARGB_8888:由4个8位组成即32位,
  • RGB_565:R为5位,G为6位,B为5位共16位,

当然位数越高代表的图片的质量就越高

createBitmap(Bitmap source):根据原有的Bitmap去创建一个新的bitmap

  • createBitmap(Bitmap source, int x, int y, int width, int height):根据原有的bitmap去创建一个从原有bitmap的(x, y)这个点起,宽为width,高为height的bitmap一般使用这个方法去裁剪原有的bitmap

  • createBitmap(Bitmap source, int x, int y, int width, int height, Matrix matrix, boolean) :根据原有的bitmap创建一个从原有bitmap的(x, y)这个点起,宽为width,高为height的bitmap,并且对该bitmap进行matrix的矩阵变换,最后一个参数是:当进行的不仅仅是平移变换时,filter为true的情况下会进行滤波处理,意思就是说让新生成的bitmap的图像质量好点,为false,就没有滤波处理

  • createBitmap(int width, int height, Config config):创建一个宽为width,高为height,图片质量为config的bitmap

  • createBitmap(DisplayMetrics displayMetrics, int width, int height, Config config):根据屏幕信息displayMetrics创建一个宽为width,高为height,图片质量为config的bitmap

  • createBitmap(DisplayMetrics displayMetrics, int[] color, int width, int height, Config onfig):根据屏幕信息displayMetrice创建一个宽为width,高为heighe,图片质量为config,并且使用颜色数组color从上而下,从左到右进行填充

  • createBitmap(DisplayMetrics, int[], int, int, int, int, Config): 这个方法和上述方法功能相近,但是有两个参数博主不知道什么意思,Android官方文档上的解释居然和上述方法相近

  • createBitmap(int[], int, int, int, int, Config):创建一个宽为width,高为heighe,图片质量为config,并且使用颜色数组color从上而下,从左到右进行填充

  • createBitmap(int[], int, int, Config):创建一个宽为width,高为heighe,图片质量为config,并且使用颜色数组color从上而下,从左到右进行填充

  • createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter):对原始的Bitmap进行缩放,最后一个参数的意思就是bitmap边缘抗锯齿。
    上述就是我们自己去创建一个一个新的Bitmap,在这里我们得说说 在我们使用裁剪Bitmap的时候createBitmap(Bitmap source, int x, int y, int width, int height)我们得记住我们开始的点x+width<=source.getWidth(),要不然会报异常。

下面我们去看看如何在资源文件当中去解析出我们的Bitmap,在我们开始解析之前,我们要在这里再次为大家介绍一个帮助类,那么这个帮助类是BitmapFactory,这个类有一个decodeXXXXX();的静态方法,通过这个方法我们就可以得到一个Bitmap对象,decodeXXXXX();主要分为四种,分别是读取文件,资源,流,byte[]数组,当中的Bitmap,而在读取的时候会有一些配置需要我们去选择,Google也给我们封装在BitmapFactory的静态内部类Options类当中,我们点进去看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public static class Options {

public Options() {
inDither = false;
inScaled = true;
inPremultiplied = true;
}

/** 维护了一个Bitmap位图对象,如果更改解码方式则,重用这个位图加载内容。 */
public Bitmap inBitmap;

/**
* 配置Bitmap是否可以更改,比如:在Bitmap上隔几个像素加一条线段
*/
@SuppressWarnings({"UnusedDeclaration"}) // used in native code
public boolean inMutable;

/**
* 如果设置为true,不获取图片,不分配内存,但会返回图片的高度宽度信息。
*/
public boolean inJustDecodeBounds;

/**
* 图片缩放的倍数
*/
public int inSampleSize;

/**
* 设置解码图片质量
*/
public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;

/**
* 为true,产生的位图将它的颜色通道pre-multipled alpha通道。
*/
public boolean inPremultiplied;

/**
* 如果为true,解码器尝试抖动解码
*/
public boolean inDither;

/**
* 用于位图的像素压缩比
*/
public int inDensity;

/**
* 用于目标位图的像素压缩比(要生成的位图)
*/
public int inTargetDensity;

/**
* 当前屏幕的像素密度
*/
public int inScreenDensity;

/**
* 设置为true时进行图片压缩,从inDensity到inTargetDensity
*/
public boolean inScaled;

/**
* 当存储Pixel的内存空间在系统内存不足时是否可以被回收
*/
@Deprecated
public boolean inPurgeable;

/**
* inPurgeable为true情况下才生效,是否可以共享一个InputStream
*/
@Deprecated
public boolean inInputShareable;

/**
* 为true则优先保证Bitmap质量其次是解码速度
*/
public boolean inPreferQualityOverSpeed;

/**
* 获取图片的宽度值
*/
public int outWidth;

/**
* 获取图片的高度值
*/
public int outHeight;

/**
* 设置解码图像
*/
public String outMimeType;

/**
* 创建临时文件,将图片存储
*/
public byte[] inTempStorage;

/**
* mCancel为true取消当前Decode
*/
public boolean mCancel;

}

读取文件当中的bitmap文件

1
2
3
4
5
6
7
public static Bitmap decodeFile(String pathName, Options opts) //从文件读取图片 

public static Bitmap decodeFile(String pathName)

public static Bitmap decodeFileDescriptor(FileDescriptor fd)//从文件读取文件 与decodeFile不同的是这个直接调用JNI函数进行读取 效率比较高

public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)

读取bitmap资源文件

1
2
3
public static Bitmap decodeResource(Resources res, int id) //从资源文件读取图片

public static Bitmap decodeResource(Resources res, int id, Options opts)

读取流中bitmap

1
2
3
public static Bitmap decodeStream(InputStream is) //从输入流读取图片

public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)

读取byte[]数组中的Bitmap

1
2
3
public static Bitmap decodeByteArray(byte[] data, int offset, int length) //从数组读取图片

public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts)

以上就是如何创建一个Bitmap文件

4、如何显示到屏幕上面,

如果我们要把我们的Bitmap显示到屏幕上我们必须要依赖View通过View进行显示,其实每一个View都是可以显示图片的,用作背景的情况下都是可以的,但是显示的是一个Drawable对象,所以有时候我们要去研究Drawable和Bitmap之间的转换问题,

注意:(这里考虑View,ViewGroup,ImagView,ImageButton等个别可以直接显示的等会再说)

1、Bitmap转换成为Drawable,

我们说过Bitmap是Drawable的一个子类,所以我们只需要通过中间BitmapDrawable去封装一下Bitmap即可

1
2
3
4
5
6
7
8
9
10
11
/** bitmap ---> Drawable */
public Drawable bitmap2Drawable(Bitmap bitmap){
BitmapDrawable bitmapDrawable = new BitmapDrawable(bitmap);
return bitmapDrawable;
}

/** bitmap ---> Drawable */
public Drawable bitmap2Drawable(Resources res, Bitmap bitmap){
BitmapDrawable bitmapDrawable = new BitmapDrawable(res, bitmap);
return bitmapDrawable;
}

2、Drawable转换成为Bitmap

我们说过Bitmap保存的是图像的数据,通过这个我们可以使用Canvas把Drawable的图像数据画到bitmap

1
2
3
4
5
6
7
8
9
/** Drawable --- > Bitmap */
public Bitmap bitmap2Drawable(Drawable drawable){
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
drawable.getOpacity() != PixelFormat.OPAQUE ? Bitmap.Config.ARGB_8888 : Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
drawable.draw(canvas);
return bitmap;
}

转换成为Drawable以后我们就可以轻松设置到随便一个View的背景显示出来了,至于ImageView,ImageButton,它则提供了直接设置Bitmap的方法,直接调用即可。

5、怎么保存我们的Bitmap文件呢!

我们先来看一下这个方法

1
2
3
public boolean compress(CompressFormat format, int quality, OutputStream stream)//按指定的图片格式以及画质,将图片转换为输出流。
format:Bitmap.CompressFormat.PNG或Bitmap.CompressFormat.JPEG
quality:画质,0-100.0表示最低画质压缩,100以最高画质压缩。对于PNG等无损格式的图片,会忽略此项设置。
1
2
3
4
5
6
7
8
9
10
11
12
13
/** 把Bitmap图片保存在本地 */
public void writeBitmapToFile(String filePath, Bitmap b, int quality) {
try {
File desFile = new File(filePath);
FileOutputStream fos = new FileOutputStream(desFile);
BufferedOutputStream bos = new BufferedOutputStream(fos);
b.compress(Bitmap.CompressFormat.JPEG, quality, bos);
bos.flush();
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}

通过上述方法不仅仅是可以保存,它是可以压缩的,我们来看看这个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 以quality画质对Bitmap进行压缩 */
private Bitmap compressImage(Bitmap image, int quality) {
if (image == null || quality < 0 || quality > 100) { return null; }
ByteArrayOutputStream baos = null;
try {
baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, quality, baos);
byte[] bytes = baos.toByteArray();
ByteArrayInputStream isBm = new ByteArrayInputStream(bytes);
Bitmap bitmap = BitmapFactory.decodeStream(isBm);
return bitmap;
} catch (OutOfMemoryError e) {
} finally {
try {
if (baos != null) {
baos.close();
}
} catch (IOException e) {
}
}
return null;
}

关于优化的问题,后面博客说吧,毕竟这篇是介绍篇,好了我们接下来看看我们的今天的测试Demo,其实我们学习关于Bitmap的各种操作方法,大多数的情况不是说是为了某种目的去操作它。而是由于OOM异常,被迫的去压缩,去裁剪,去降低图片质量,已达到最优的情况,最优的情况包括,首先程序不能崩,速度最快,用户体验不能卡之类的。所以我打算我的这个Demo就主要去对这一方面去写点东西。

首先我们来看一下效果吧

高斯模糊

生成一种毛玻璃的效果,在这里我要说的是每一种毛玻璃的效果都有一种模糊算法在支持,有的模糊算法是C写的,有的是用Java写的,这里面有很大的学问,这里就不要深究啦总之通过原图的Bitmap生成模糊的Bitmap很吃内存,没办法,我们首先对Bitmap缩小 去生成模糊bitmap 然后再去放大,有可能还会用到压缩Bitmap,最后以ImageView为载体显示出来,所作的一切都是被迫的,这种情况会在以后发生很多,好了看代码吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
/**
* 通过Java去实现的模糊算法 不是博主写的,
* Created by jay on 11/7/15.
*/
public class FastBlurUtil {

public static Bitmap doBlur(Bitmap sentBitmap, int radius, boolean canReuseInBitmap) {

Bitmap bitmap;
if (canReuseInBitmap) {
bitmap = sentBitmap;
} else {
bitmap = sentBitmap.copy(sentBitmap.getConfig(), true);
}

if (radius < 1) {
return (null);
}

int w = bitmap.getWidth();
int h = bitmap.getHeight();

int[] pix = new int[w * h];
bitmap.getPixels(pix, 0, w, 0, 0, w, h);

int wm = w - 1;
int hm = h - 1;
int wh = w * h;
int div = radius + radius + 1;

int r[] = new int[wh];
int g[] = new int[wh];
int b[] = new int[wh];
int rsum, gsum, bsum, x, y, i, p, yp, yi, yw;
int vmin[] = new int[Math.max(w, h)];

int divsum = (div + 1) >> 1;
divsum *= divsum;
int dv[] = new int[256 * divsum];
for (i = 0; i < 256 * divsum; i++) {
dv[i] = (i / divsum);
}

yw = yi = 0;

int[][] stack = new int[div][3];
int stackpointer;
int stackstart;
int[] sir;
int rbs;
int r1 = radius + 1;
int routsum, goutsum, boutsum;
int rinsum, ginsum, binsum;

for (y = 0; y < h; y++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
for (i = -radius; i <= radius; i++) {
p = pix[yi + Math.min(wm, Math.max(i, 0))];
sir = stack[i + radius];
sir[0] = (p & 0xff0000) >> 16;
sir[1] = (p & 0x00ff00) >> 8;
sir[2] = (p & 0x0000ff);
rbs = r1 - Math.abs(i);
rsum += sir[0] * rbs;
gsum += sir[1] * rbs;
bsum += sir[2] * rbs;
if (i > 0) {
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
} else {
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}
}
stackpointer = radius;

for (x = 0; x < w; x++) {

r[yi] = dv[rsum];
g[yi] = dv[gsum];
b[yi] = dv[bsum];

rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;

stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];

routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];

if (y == 0) {
vmin[x] = Math.min(x + radius + 1, wm);
}
p = pix[yw + vmin[x]];

sir[0] = (p & 0xff0000) >> 16;
sir[1] = (p & 0x00ff00) >> 8;
sir[2] = (p & 0x0000ff);

rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];

rsum += rinsum;
gsum += ginsum;
bsum += binsum;

stackpointer = (stackpointer + 1) % div;
sir = stack[(stackpointer) % div];

routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];

rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];

yi++;
}
yw += w;
}
for (x = 0; x < w; x++) {
rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0;
yp = -radius * w;
for (i = -radius; i <= radius; i++) {
yi = Math.max(0, yp) + x;

sir = stack[i + radius];

sir[0] = r[yi];
sir[1] = g[yi];
sir[2] = b[yi];

rbs = r1 - Math.abs(i);

rsum += r[yi] * rbs;
gsum += g[yi] * rbs;
bsum += b[yi] * rbs;

if (i > 0) {
rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];
} else {
routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];
}

if (i < hm) {
yp += w;
}
}
yi = x;
stackpointer = radius;
for (y = 0; y < h; y++) {
// Preserve alpha channel: ( 0xff000000 & pix[yi] )
pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum];

rsum -= routsum;
gsum -= goutsum;
bsum -= boutsum;

stackstart = stackpointer - radius + div;
sir = stack[stackstart % div];

routsum -= sir[0];
goutsum -= sir[1];
boutsum -= sir[2];

if (x == 0) {
vmin[y] = Math.min(y + r1, hm) * w;
}
p = x + vmin[y];

sir[0] = r[p];
sir[1] = g[p];
sir[2] = b[p];

rinsum += sir[0];
ginsum += sir[1];
binsum += sir[2];

rsum += rinsum;
gsum += ginsum;
bsum += binsum;

stackpointer = (stackpointer + 1) % div;
sir = stack[stackpointer];

routsum += sir[0];
goutsum += sir[1];
boutsum += sir[2];

rinsum -= sir[0];
ginsum -= sir[1];
binsum -= sir[2];

yi += w;
}
}

bitmap.setPixels(pix, 0, w, 0, 0, w, h);

return (bitmap);
}
}

来看看我们的Layout:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent"
>

<ImageView
android:layout_width="match_parent"
android:layout_weight="1"
android:src="@mipmap/index"
android:layout_margin="20dip"
android:layout_height="0dip"
/>

<ImageView
android:id="@+id/iv_content"
android:layout_margin="20dip"
android:layout_width="match_parent"
android:layout_weight="1"
android:layout_height="0dip" />

</LinearLayout>

和以前一样,上面的为原始图,下面的为模糊图

activity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}

/** 初始化View */
private void initView() {
ImageView mImage = (ImageView) findViewById(R.id.iv_content);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.index).
copy(Bitmap.Config.ARGB_4444, true);
mImage.setImageBitmap(getBitmapBlur(bitmap, 10));
}

/** 对Bitmap进行处理 */
private Bitmap initBitmap(Bitmap bitmap, boolean isup) {
int width;
int height;
if(isup){
width = bitmap.getWidth() * 6;
height = bitmap.getHeight() * 6;
}else{
width = bitmap.getWidth() / 6;
height = bitmap.getHeight() / 6;
}
return Bitmap.createScaledBitmap(bitmap, width, height, false);
}

/** 初始化Bitmap */
private Bitmap getBitmapBlur(Bitmap bitmap, int level) {
if(level > 0){
Bitmap bitmap1 = FastBlurUtil.doBlur(initBitmap(bitmap, false), level, true);
return initBitmap(bitmap1, true) ;
}
return initBitmap(bitmap, true);

效果就是上述,关于内存的东西,后面会提到,