首先要明确的一点是,一张图片的在内存中存储所需的大小和图片在屏幕设备上完整显示所需的内存大小是有非常大的差异的。
如下图,从中我们可以清晰的看出这张图片,在硬盘上所占的存储空间是303KB,也就是310272个字节。但是如果想把这样图片完整的展示到屏幕设备上,所需的内存空间远远不止这些。
有这样一个计算公式:图片展示所需内存 = 图片的宽度像素 × 图片的高度像素 × 每个像素的大小
那么,像素大小怎么知道呢?当我们点击图片另存为时,会出现下图:
依次来介绍一下其中的含义。
如果在Andorid中,系统为应用默认提供的VM Heap是16M,在不对图片进行压缩处理的情况下,一定会出现OOM异常。
创建一个工程,在布局中加一个ImageView
控件,并在MainActivity
中找到控件,设置图片。
ImageView iv = (ImageView) findViewById(R.id.iv);
Bitmap bitmap = BitmapFactory.decodeFile("/mnt/sdcard/dog.jpg");
iv.setImageBitmap(bitmap );
可以看到,应用程序向系统申请了30720012个字节,然后就直接出现了OutOfMemoryError错误。
在下一节中将讲述如何解决加载大图而不出现OOM的方法。
我们可以看到,这样狗狗的图片是2400*3200的,而我们的手机只是320*480。如果完全直接放上去,一来是会出现异常;二来是浪费资源。
Google工程师已经我们准备好了解决办法,再使用BitmapFactory
去解析资源时,先获取被加载图片的宽高,并结合手机设备的屏幕宽高计算出缩放比例,然后再去使用这个缩放比例加载图片资源到内存中。
代码也比较简单,请看:
ImageView iv = (ImageView) findViewById(R.id.iv);
// ★1. 使用窗口管理者,获取手机屏幕的宽高
WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
int screenHeight = wm.getDefaultDisplay().getHeight();
int screenWidth = wm.getDefaultDisplay().getWidth();
// ★2. 在不把图片加载入内存的情况下,获取图片的属性和配置
BitmapFactory.Options options = new Options();
// 此参数设置为true是,使用BitmapFactory解析资源并不会返回Bitmap,但是资源的相关配置却会被设置:例如图片的宽高
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile("/mnt/sdcard/dog.jpg", options);
// 拿到图片的宽高
int outHeight = options.outHeight;
int outWidth = options.outWidth;
// ★3. 根据屏幕宽高和图片宽高计算出缩放比例
int scale = 0;
int scaleH = outHeight / screenHeight;
int scaleW = outWidth / screenWidth;
scale = scaleH > scaleW ? scaleH : scaleW;
// ★4. 根据缩放比去解析图片资源,并返回Bitmap
options.inJustDecodeBounds = false;
options.inSampleSize = scale;
Bitmap bitmap = BitmapFactory.decodeFile("/mnt/sdcard/dog.jpg", options);
iv.setImageBitmap(bitmap);
在Android加载到内存的Bitmap是不允许修改的,只能够在其副本上修改和作画。那么如何创建一个原图的副本呢?
代码也比较简单,其中涉及了Canvas
、Paint
、Matrix
等类,下面是简单的拷贝原图的代码:
ImageView srcImageView = (ImageView) findViewById(R.id.iv_src);
ImageView copyImageView = (ImageView) findViewById(R.id.iv_copy);
// 原图
Bitmap srcBitmap = BitmapFactory.decodeFile("/mnt/sdcard/meinv.jpg");
srcImageView.setImageBitmap(srcBitmap);
// 拷贝
// 1. 准备一个和原图宽高完全一样的白纸
Bitmap copyBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), srcBitmap.getConfig());
// 2. 把白纸放在画布上
Canvas canvas = new Canvas(copyBitmap);
// 3. 准备一支笔
Paint paint = new Paint();
// 4. 准备一个矩阵
Matrix matrix = new Matrix();
// 使用指定的矩阵绘图
canvas.drawBitmap(srcBitmap, matrix, paint);
copyImageView.setImageBitmap(copyBitmap);
就如在学动画的时候,图形的处理操作也分为以下种类,操作的关键步骤是使用矩阵进行变化;Google工程师已经帮我们把这些操作封装的很完善了。
代码如下:
// 原图
Bitmap srcBitmap = BitmapFactory.decodeFile("/mnt/sdcard/meinv.jpg");
srcImageView.setImageBitmap(srcBitmap);
// 拷贝
Bitmap copyBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), srcBitmap.getConfig());
Canvas canvas = new Canvas(copyBitmap);
Paint paint = new Paint();
Matrix matrix = new Matrix();
// ★使用矩阵平移图形
matrix.setTranslate(150, 50);
canvas.drawBitmap(srcBitmap, matrix, paint);
copyImageView.setImageBitmap(copyBitmap);
缩放代码如下:
// ★使用矩阵缩放图形
// 以图片中心点为原点,缩小0.5倍
matrix.setScale(0.5f, 0.5f, srcBitmap.getWidth() / 2,
srcBitmap.getHeight() / 2);
旋转代码如下:
// ★使用矩阵图形
// 以图片中心点为原点,旋转30度
matrix.setRotate(30, srcBitmap.getWidth()/2,
srcBitmap.getHeight()/2);
镜像代码如下:
// ★使用矩阵把图片镜像
matrix.setScale(-1.0f, 1.0f);
matrix.postTranslate(srcBitmap.getWidth(), 0);
测试图:
倒影:
倒影代码如下:
// ★使用矩阵把图片倒影
matrix.setScale(1.0f, -1.0f);
matrix.postTranslate(0 , srcBitmap.getHeight());
目的,使用使用颜色矩阵,修改图片的色值(简单+简陋=练练手):
布局下方是三个<SeekBar>
布局,用于修改图片的色值。
代码如下:
public class MainActivity extends Activity implements OnSeekBarChangeListener {
private Bitmap srcBitmap;
private SeekBar sb_red;
private SeekBar sb_green;
private SeekBar sb_blue;
private ImageView iv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.activity_main);
sb_red = (SeekBar) findViewById(R.id.sb_red);
sb_green = (SeekBar) findViewById(R.id.sb_green);
sb_blue = (SeekBar) findViewById(R.id.sb_blue);
iv = (ImageView) findViewById(R.id.iv);
srcBitmap = BitmapFactory.decodeFile("/mnt/sdcard/meinv.jpg");
iv.setImageBitmap(srcBitmap);
sb_blue.setOnSeekBarChangeListener(this);
sb_red.setOnSeekBarChangeListener(this);
sb_green.setOnSeekBarChangeListener(this);
}
// 停止滑动时
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
int currentPosition = seekBar.getProgress();
Bitmap copyBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), srcBitmap.getConfig());
Canvas canvas = new Canvas(copyBitmap);
Paint paint = new Paint();
// ★使用颜色矩阵绘制绘制图片
ColorMatrix colorMatrix = new ColorMatrix();
float rf = 0;
float gf = 0;
float bf = 0;
// 根据滑动的值修改ARGB的颜色值
switch (seekBar.getId()) {
case R.id.sb_red:
rf = currentPosition / 10f;
break;
case R.id.sb_blue:
bf = currentPosition / 10f;
break;
case R.id.sb_green:
gf = currentPosition / 10f;
break;
}
colorMatrix.set(new float[] { //
rf, 0, 0, 0, 0,//
0, gf, 0, 0, 0,//
0, 0, bf, 0, 0,//
0, 0, 0, 1, 0 });
ColorFilter filter = new ColorMatrixColorFilter(colorMatrix);
// ★为画笔设置颜色矩阵过滤器
paint.setColorFilter(filter);
canvas.drawBitmap(srcBitmap, new Matrix(), paint);
iv.setImageBitmap(copyBitmap);
}
// 滑动的过程中
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
}
// 开始滑动时
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
}
测试结果:
没什么很重要的东西,知道一下颜色矩阵就行。
自己做出来后还是挺逗的。
ImageView
的一个触摸事件;在按下和移动时,记录触摸点,并绘制图画和设置到
Imageview
上。
那么下面就一步一步来吧
第一步,准备一个背景图,并存入到SD卡中。
设置背景图片的代码:
iv = (ImageView) findViewById(R.id.iv);
srcBitmap = BitmapFactory.decodeFile("/mnt/sdcard/bk.png");
iv.setImageBitmap(srcBitmap);
第二步,做一个背景图的拷贝,以便在其上面进行绘画
// 获取一个拷贝
copyBitmap = Bitmap.createBitmap(srcBitmap.getWidth(), srcBitmap.getHeight(), srcBitmap.getConfig());
// 获取画布
canvas = new Canvas(copyBitmap);
// 获取画笔
paint = new Paint();
// 获取一个矩阵
matrix = new Matrix();
canvas.drawBitmap(srcBitmap, matrix, paint);
// 画一条直线
// canvas.drawLine(0, 0, 100, 100, paint);
iv.setImageBitmap(copyBitmap);
// 注册触摸事件回调
iv.setOnTouchListener(this);
第三步,注册一个ImageView
的一个触摸事件;在按下和移动时,记录触摸点,并绘制图画和设置到Imageview
上。
// 起始坐标点
private int startX;
private int startY;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = (int) event.getX();
startY = (int) event.getY();
System.out.println("按下的坐标点(" + startX + "," + startY + ")");
break;
case MotionEvent.ACTION_MOVE:
int nowX = (int) event.getX();
int nowY = (int) event.getY();
System.out.println("移动起始的坐标点(" + startX + "," + startY + ")");
System.out.println("移动结束的坐标点(" + nowX + "," + nowY + ")");
canvas.drawLine(startX, startY, nowX, nowY, paint);
iv.setImageBitmap(copyBitmap);
startX = nowX;
startY = nowY;
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
// 返回值为true,事件被消耗掉,并且不在向下传递;
// 返回值为false,事件不被消耗掉,并且向下继续传递;
return true;
}
第四步,增加“加粗画笔”和“改变颜色的功能”
// 颜色内容
int[] colors = { Color.RED, Color.GREEN, Color.BLUE };
private int colorIndex;
// 修改颜色
public void changecolor(View v) {
paint.setColor(colors[colorIndex++ % 3]);
}
// 画笔宽度
private int paintWidth;
// 加粗画笔
public void overstriking(View v) {
paint.setStrokeWidth(++paintWidth);
}