计算机图形表示的原理
首先要明确的一点是,一张图片的在内存中存储所需的大小和图片在屏幕设备上完整显示所需的内存大小是有非常大的差异的。
如下图,从中我们可以清晰的看出这张图片,在硬盘上所占的存储空间是303KB,也就是310272个字节。但是如果想把这样图片完整的展示到屏幕设备上,所需的内存空间远远不止这些。
有这样一个计算公式:图片展示所需内存 = 图片的宽度像素 × 图片的高度像素 × 每个像素的大小
那么,像素大小怎么知道呢?当我们点击图片另存为时,会出现下图:
依次来介绍一下其中的含义。
- 单色位图:
- 要么黑、要么白,只需要一bit就可以表示,需要1/8个字节来表示一个像素值。在这种情况下上面的图片显示到加载入内存中需要:1920 * 1200 * 1/8 / 1024 / 1024= 0.2M。
- 16位图:
- 需要1/2一个字节来表示一个像素值。在这种情况下图片加载入内存准备显示需要:1920 * 1200 * 1 / 2 / 1024 / 1024 = 1.09M。
- 256位图:
- 表示一个像素值需要用1个字节来表示,在这种情况下图片加载入内存准备显示需要:1920 * 1200 * 1 / 1024 / 1024 = 2.19M。
- 24位位图:
- 表示一个像素值需要3个字节来表示,在这种情况下图片加载入内存准备显示需要:1920 * 1200 * 3 / 1024 / 1024 = 6.59M。
- 而在Android操作系统中,使用的是ARGB来表示一个像素值
- 其中A代表的是透明度;那么在这种情况下,在这种情况下图片加载入内存准备显示需要:1920 * 1200 * 4 / 1024 / 1024 = 8.79M。
如果在Andorid中,系统为应用默认提供的VM Heap是16M,在不对图片进行压缩处理的情况下,一定会出现OOM异常。
加载大图出现OOM
让我们加载一张大图试试吧,图片资源如下,大小为1.7M。
创建一个工程,在布局中加一个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);
测试结果如下:
图形处理的常用的API
就如在学动画的时候,图形的处理操作也分为以下种类,操作的关键步骤是使用矩阵进行变化;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); }