作为一名码农,我们平时对图片的处理基本上是家常便饭的,比如加载到页面上,或者上传的服务器,但是相机拍出来的照片往往是高清的,加载起来会占用我们大量内存空间,为了减小图片的体积,我们往往需要将图片进行压缩,这就需要我们了解一下关于图片压缩的知识,那下面开始我们正题。
首先我们需要认识了一些基本概念:
先说一下位图的基本知识点
图片资源的存储方式:
- 以File的形式存在于SD卡中
- 以Stream的形式存在于内存中
- 以Bitmap形式存在于内存中
位图不同的色彩模式在内存中的占用空间计算:
- ALPHA_8:每个像素占用1byte内存
- ARGB_4444:每个像素占用2byte内存
- ARGB_8888:每个像素占用4byte内存
- RGB_565:每个像素占用2byte内存
举个例子,如果一个图片的分辨率是1024 * 768,采用Argb_8888,那么占用的控件就是1024 * 768 * 4=3MB
那以上知识就能解决我们之前常遇到的问题:为什么资源文件转化为Bitmap时大小会突然变大呢?
以任意一张图片为例, 我本地存了一张分辨率为750 * 1334,大小为119k。如果将这张图片以bitmap形式加载到内存中,它占用的大小是多少,如何计算呢?它的计算公式:
图片的占用内存 = 图片的长度(像素单位) * 图片的宽度(像素单位) * 单位像素所占字节数
其中单位占用字节数的大小是变化的,由BitmapFactory.Options的inPreferredConfig,也就是图片的色彩模式决定,一般情况下默认为:ARGB_8888,由此:我们就可以计算出此图片以bitmap形式加载消耗内存为:750 * 1334 * 4=3.81M,这还是只是一张普通的图骗,现在手机拍的高清图基本上都是2000 * 2000 * 4 = 15M+ 了。所以如果不做任何处理的图片大量使用到我们APP上,结局你懂得,现在就知道为了需要压缩图片了吧。
好的,我们下面再介绍一下我们压缩图片会使用到的一些方法
BitmapFactory
Matrix
ExifInterface
大致会设计到以上知识点,当然也包括一些我们平时常用的一些基本类
File
Bitmap
Handler
AsyncTask
Iterator
如果对上面五个概念不是特别清楚的,还是希望读者可以先去查询一下,方便本文代码的理解。
好,现在来一一解释以下各个类的具体作用
BitmapFactory
顾名思义,这个类的作用是为了产生Bitmap的,包括以下可以使用的方法:
BitmapFactory.decodeFile(); //从文件中加载Bitmap对象
BitmapFactory.decodeResource ();//从资源中加载Bitmap对象
BitmapFactory.decodeStream (); //从输入流中加载Bitmap对象
BitmapFactory.decodeByteArray //从字节数组中加载Bitmap对象
作用就是可以获取不同的对象转换成我们需要的Bitmap
这个将协助BitmapFactory完成我们的图片资源转换成Bitmap的工作。
这些获取方式里面我们会涉及到一个概念
BitmapFactory.Options
好了,下面看看BitmapFactory.Options的常用参数
inJustDecodeBounds
如果将这个值置为true,那么在解码的时候将不会返回bitmap,只会返回这个bitmap的尺寸。这个属性的目的是,如果你只想知道一个bitmap的尺寸,但又不想将其加载到内存时。这是一个非常有用的属性。
inSampleSize
缩放比例。当它小于1的时候,将会被当做1处理,这个参数需要是2的幂函数。例如,width=100,height=100,inSampleSize=2,那么就会将bitmap处理为,width=50,height=50,宽高各降为1 / 2,像素数降为1 / 4,通过这种方式的压缩我们称之为采样率压缩或者尺寸压缩,但需要注意的是尺寸压缩会改变图片的尺寸,即压缩图片宽度和高度的像素点,这样会降低图片资源转化为bitmap内存的占用,从而一定程度上减少OOM的概率。但是要注意,如果压缩比太大,也会由于像素点降低导致图片失真严重,最后图片有高清成了马赛克,所以通常我们是根据我们展示图片的控件大小来设置这个值的变化,以此达到最适宜的压缩程度,还有一点就是采样率为整数,且为2的n次幂,n可以为0。即采样率为1,处理后的图片尺寸与原图一致。当采样率为2时,即宽、高均为原来的1/2,像素则为原来的1/4.其占有内存也为原来的1/4。当设置的采样率小于1时,其效果与1一样。当设置的inSampleSize大于1,不为2的指数时,系统会向下取一个最接近2的指数的值。
inPreferredConfig
这个值就是用来设置位图的色彩模式了,默认值是ARGB_8888,在这个模式下,一个像素点占用4bytes空间,一般对透明度不做要求的话,一般采用RGB_565模式,这个模式下一个像素点占用2bytes。
outWidth和outHeight
表示这个Bitmap的宽和高,一般和inJustDecodeBounds一起使用来获得Bitmap的宽高,但是不加载到内存。
上面基本上就是使用BitmapFactory.Options对图片进行压缩处理需要使用到的一些概念,当然Bitmap本身也自带一个压缩方法:
Bitmap.compress(CompressFormat format, int quality, OutputStream stream)
简单解释一下这三个参数,第一个表示Bitmap被压缩成的图片格式;第二个表示压缩的质量控制,范围0~100,很好理解。quality为80,表示压缩为原来80%的质量效果。有些格式,例如png,它是无损的,这个值设置了也是无效的。因为质量压缩是在保持像素前提下改变图片的位深及透明度等来压缩图片的,所以quality值与最后生成的图片的大小并不是线性关系,这种方法我们称之为质量压缩。
Matrix
我们称之为矩阵,听着名字就知道和数学有关系,在高等数学里有介绍,然而在图像处理方面,主要是用于图形的缩放、平移、旋转,倾斜等操作,具体的数学原理也不是我们的关注重点,重要的是如何去使用它。
看看它自带的一些方法
Matrix.setScale Matrix.postScale Matrix.preScale
Matrix.setTranslate Matrix.postTranslate Matrix.preTranslate
Matrix.setRotate Matrix.postRotate Matrix.preRotate
Matrix.setSkew Matrix.postSkew Matrix.preSkew
上面四种类型的方法,分别就对应Matrix的四种操作,但是每种分成了3种实现方式,那么为什么会有三种呢,我们什么场景下用set、什么场景下用pre、什么场景下用post呢?又有什么区别呢?其实在我们使用set、post、pre等一系列方法的时候,这些方法会被加入到一个队里当中顺序执行的,当我们调用set的时候,该队里就会被清空,并且把set放入到队列的中间,然后pre总是总是放入到set的最前面,post则会放到set的最后面。这样说可能有点抽象
,这个时候我们就需要举个例子:
第一种
Matrix m = new Matrix();
m.setRotate(45);
m.setTranslate(80, 80);
//只有m.setTranslate(80, 80)有效,因为m.setRotate(45);被清除.
第二种
atrix m = new Matrix();
m.setTranslate(80, 80);
m.postRotate(45);
//先执行m.setTranslate(80, 80);后执行m.postRotate(45);
第三种
Matrix m = new Matrix();
m.setTranslate(80, 80);
m.preRotate(45);
//先执行m.setTranslate(80, 80);后执行m.preRotate(45);
第四种
Matrix m = new Matrix();
m.preScale(2f,2f);
m.preTranslate(50f, 20f);
m.postScale(0.2f, 0.5f);
m.postTranslate(20f, 20f);
//执行顺序:m.preTranslate(50f, 20f)-->m.preScale(2f,2f)-->m.postScale(0.2f, 0.5f)-->m.postTranslate(20f, 20f)
//注意:m.preTranslate(50f, 20f)比m.preScale(2f,2f)先执行,因为它查到了队列的最前端.
第五种
Matrix m = new Matrix();
m.postTranslate(20, 20);
m.preScale(0.2f, 0.5f);
m.setScale(0.8f, 0.8f);
m.postScale(3f, 3f);
m.preTranslate(0.5f, 0.5f);
//执行顺序:m.preTranslate(0.5f, 0.5f)-->m.setScale(0.8f, 0.8f)-->m.postScale(3f, 3f)
//注意:m.setScale(0.8f, 0.8f)清除了前面的m.postTranslate(20, 20)和m.preScale(0.2f, 0.5f);
最终就是一个前后执行的顺序问题,我们也不需要太过纠结,懂个意思即可,我们这里的压缩图片也只用到了旋转,这里其他就不在赘述了。
ExifInterface
ExifInterface是Android为我们提供的一个支持库,在build.gradle文件中引入下面的代码,便可以使用ExifInterface了。用于获取图片的一些信息,包括:分辨率,旋转方向,感光度、白平衡、拍摄的光圈、焦距、分辨率、相机品牌、型号、GPS等信息,这里我们利用这个和Matrix结合来完成图片的旋转,更多细节的话同学们可以去自行搜索,这里也不再提了。
下面给出具体的压缩部分代码:
private Bitmap decodeImgFromResource(Context context, int resId, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; //测量宽高时,不需要加载到内存中
BitmapFactory.decodeResource(context.getResources(), resId, options);
// 调用calculateInSampleSize计算inSampleSize值
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
//将inPreferredConfig设置为RGB_565, 将会进一步降低图片大小,但不推荐使用有透明度要求的图片
options.inPreferredConfig = Bitmap.Config.RGB_565;
// 使用获取到的inSampleSize值再次解析图片
options.inJustDecodeBounds = false;
//推荐使用decodeStream,是因为相对来说加载图片时会占据更小内存
return BitmapFactory.decodeResource(context.getResources(), resId, options);
}
其中的calculateInSampleSize()为:
//按照预期宽高计算图片适当的缩放比例,缩放比例总为2的倍数
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
//计算inSampleSize直到缩放后的宽高都小于指定的宽高
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
用于计算出图片适当压缩比例,这里用的是指定宽高进行压缩,如果没有指定可以利用以下方法:
//自己计算图片适当的缩放比例,和鲁班压缩一致
private int calculateInSampleSize(BitmapFactory.Options options) {
int srcWidth = options.outWidth;
int srcHeight = options.outHeight;
srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;
srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;
int longSide = Math.max(srcWidth, srcHeight);
int shortSide = Math.min(srcWidth, srcHeight);
float scale = ((float) shortSide / longSide);
if (scale <= 1 && scale > 0.5625) {
if (longSide < 1664) {
return 1;
} else if (longSide < 4990) {
return 2;
} else if (longSide > 4990 && longSide < 10240) {
return 4;
} else {
return longSide / 1280 == 0 ? 1 : longSide / 1280;
}
} else if (scale <= 0.5625 && scale > 0.5) {
return longSide / 1280 == 0 ? 1 : longSide / 1280;
} else {
return (int) Math.ceil(longSide / (1280.0 / scale));
}
}
然后就是图片的旋转处理:
//旋转图片到合适角度
private Bitmap rotatingImage(File fromFile, Bitmap bitmap) throws IOException {
ExifInterface srcExif;
srcExif = new ExifInterface(new FileInputStream(fromFile));
if (srcExif == null) {
return bitmap;
}
Matrix matrix = new Matrix();
int angle = 0;
int orientation = srcExif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
angle = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
angle = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
angle = 270;
break;
}
matrix.postRotate(angle);
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
当然我们的压缩过程是耗时的,所以我们把它放在了线程中进行处理:
private void launch(final Context context) {
Iterator<File> fileIterator = files.iterator();
Iterator<Integer> integerIterator = bitmaps.iterator();
while (fileIterator.hasNext()) {
final File file = fileIterator.next();
AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
File result;
try {
result = decodeImgFromResource(context, file); //执行压缩操作,获取结果
handler.sendMessage(handler.obtainMessage(MSG_IMGCOMPRESS_FILE_SUCCESS, result));
} catch (IOException e) { //发送压缩失败消息
handler.sendMessage(handler.obtainMessage(MSG_IMGCOMPRESS_ERROR, e));
Log.d(LOGS, "error " + e.getMessage());
}
}
});
fileIterator.remove();
}
while (integerIterator.hasNext()) {
final Integer resId = integerIterator.next();
AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
try {
Bitmap result;
result = decodeImgFromResource(context, resId); //执行压缩操作,获取结果
handler.sendMessage(handler.obtainMessage(MSG_IMGCOMPRESS_BITMAP_SUCCESS, result));
} catch (Exception e) { //发送压缩失败消息
handler.sendMessage(handler.obtainMessage(MSG_IMGCOMPRESS_ERROR, e));
Log.d(LOGS, "error " + e.getMessage());
}
}
});
integerIterator.remove();
}
}
利用了handler进行通知,最终把图片携带到主线程给我们进行使用。
下面我们看一下基本的使用吧:
ImgCompress.with(this).load(file).setOnComPress(new ImgCompress.OnCompress() {
@Override
public void compressBitmapSuccess(Bitmap bitmap) {
}
@Override
public void compressFileSuccess(File file) {
}
@Override
public void compressFail(Throwable message) {
}
}).launch();
这里load我们可以放file文件,也可以放本地资源文件,放file最终会调用compressFileSuccess,放本地资源文件会调用compressBitmapSuccess,上面是压缩一张图片,那如果我们需要压缩大量图片怎么呢?使用如下:
ImgCompress.QueueCache queueCache = ImgCompress.with(this);
List<String> imgList = new ArrayList<>();
for (int i = 0; i < imgList.size(); i++) {
queueCache.load(new File(imgList.get(i))).setOnComPress(new ImgCompress.OnCompress() {
@Override
public void compressBitmapSuccess(Bitmap bitmap) {
}
@Override
public void compressFileSuccess(File file) {
}
@Override
public void compressFail(Throwable message) {
}
}).launch();
}
queueCache 只需要实例化一次就可以了,每次调用后面的代码即可,整体的代码也不是很长,这里就直接贴出了,大家粘贴复制就可以直接使用。
/*
*图片压缩工具
*使用方法:传入int资源,压缩后为bitmap,传入图片文件资源,压缩后仍为图片文件
*
* 2018/12/19 QQ:2381144912 WANXUEDONG
*/
public class ImgCompress implements Handler.Callback {
private static final int MSG_IMGCOMPRESS_BITMAP_SUCCESS = 1000;
private static final int MSG_IMGCOMPRESS_FILE_SUCCESS = 1001;
private static final int MSG_IMGCOMPRESS_ERROR = 1002;
private static final String LOGS = "imgcompress"; //错误查看log
private static final String FIELPOSITION = "xdw_cache_imgcompress"; //产生图片所在的父级目录名称
private static List<File> files;
private static List<Integer> bitmaps;
private Handler handler;
private OnCompress onCompress;
private ImgCompress(QueueCache cache) {
this.onCompress = cache.onCompress;
files = cache.files;
bitmaps = cache.bitmaps;
handler = new Handler(Looper.getMainLooper(), this);
}
public static QueueCache with(Context context) {
return new QueueCache(context);
}
//将每个压缩当成一个事件放到队列中
public static class QueueCache {
private Context context;
private List<File> files;
private List<Integer> bitmaps;
private OnCompress onCompress;
public QueueCache(Context context) {
this.context = context;
files = new ArrayList<>();
bitmaps = new ArrayList<>();
}
public QueueCache load(int resId) {
bitmaps.add(resId);
return this;
}
public QueueCache load(File file) {
files.add(file);
return this;
}
public void launch() {
new ImgCompress(this).launch(context);
}
public QueueCache setOnComPress(OnCompress onComPress) {
this.onCompress = onComPress;
return this;
}
}
private void launch(final Context context) {
Iterator<File> fileIterator = files.iterator();
Iterator<Integer> integerIterator = bitmaps.iterator();
while (fileIterator.hasNext()) {
final File file = fileIterator.next();
AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
File result;
try {
result = decodeImgFromResource(context, file);
handler.sendMessage(handler.obtainMessage(MSG_IMGCOMPRESS_FILE_SUCCESS, result));
} catch (IOException e) {
handler.sendMessage(handler.obtainMessage(MSG_IMGCOMPRESS_ERROR, e));
Log.d(LOGS, "error " + e.getMessage());
}
}
});
fileIterator.remove();
}
while (integerIterator.hasNext()) {
final Integer resId = integerIterator.next();
AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
try {
Bitmap result;
result = decodeImgFromResource(context, resId);
handler.sendMessage(handler.obtainMessage(MSG_IMGCOMPRESS_BITMAP_SUCCESS, result));
} catch (Exception e) {
handler.sendMessage(handler.obtainMessage(MSG_IMGCOMPRESS_ERROR, e));
Log.d(LOGS, "error " + e.getMessage());
}
}
});
integerIterator.remove();
}
}
@Override
public boolean handleMessage(Message msg) {
if (onCompress == null) {
return false;
}
switch (msg.what) {
case MSG_IMGCOMPRESS_FILE_SUCCESS:
onCompress.compressFileSuccess((File) msg.obj);
break;
case MSG_IMGCOMPRESS_BITMAP_SUCCESS:
onCompress.compressBitmapSuccess((Bitmap) msg.obj);
break;
case MSG_IMGCOMPRESS_ERROR:
onCompress.compressFail((Throwable) msg.obj);
break;
}
return false;
}
private Bitmap decodeImgFromResource(Context context, int resId) {
return decodeImgFromResource(context, resId, 100, 100);
}
//传入将要压缩的图片位置,将会返还一个压缩后的新的图片文件
private File decodeImgFromResource(Context context, File file) throws IOException {
return decodeImgFromResource(context, file, 100, 100);
}
//获取压缩图片,下面先采用了采样率压缩(又称尺寸压缩),RGB_565压缩(适用对透明度无要求的图片)
//需要注意的是质量压缩的特点是: File形式的图片被压缩了,
// 但是当你重新读取压缩后的file为Bitmap是,它占用的内存并没有改变
private Bitmap decodeImgFromResource(Context context, int resId, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; //测量宽高时,不需要加载到内存中
BitmapFactory.decodeResource(context.getResources(), resId, options);
// 调用calculateInSampleSize计算inSampleSize值
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
//将inPreferredConfig设置为RGB_565, 将会进一步降低图片大小,但不推荐使用有透明度要求的图片
options.inPreferredConfig = Bitmap.Config.RGB_565;
// 使用获取到的inSampleSize值再次解析图片
options.inJustDecodeBounds = false;
//推荐使用decodeStream,是因为相对来说加载图片时会占据更小内存
return BitmapFactory.decodeResource(context.getResources(), resId, options);
}
//传入将要压缩的图片位置,将会返还一个压缩后的新的图片文件
private File decodeImgFromResource(Context context, File file, int reqWidth, int reqHeight) throws IOException {
File tagImgFile = new File(makeName(context));
if (tagImgFile != null && file != null) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calculateInSampleSize(options);
Bitmap tagBitmap = BitmapFactory.decodeStream(new FileInputStream(file), null, options);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
tagBitmap = rotatingImage(file, tagBitmap);
tagBitmap.compress(Bitmap.CompressFormat.JPEG, 60, stream);
tagBitmap.recycle();
FileOutputStream fos = new FileOutputStream(tagImgFile);
fos.write(stream.toByteArray());
fos.flush();
fos.close();
stream.close();
}catch (Exception e){
Log.e("decodeImgFromResource", "3" + e.getMessage());
}
return tagImgFile;
} else {
Log.e(LOGS, "can't make a file position!");
return null;
}
}
//按照预期宽高计算图片适当的缩放比例,缩放比例总为2的倍数
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
//计算inSampleSize直到缩放后的宽高都小于指定的宽高
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
//自己计算图片适当的缩放比例,和鲁班压缩一致
private int calculateInSampleSize(BitmapFactory.Options options) {
int srcWidth = options.outWidth;
int srcHeight = options.outHeight;
srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;
srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;
int longSide = Math.max(srcWidth, srcHeight);
int shortSide = Math.min(srcWidth, srcHeight);
float scale = ((float) shortSide / longSide);
if (scale <= 1 && scale > 0.5625) {
if (longSide < 1664) {
return 1;
} else if (longSide < 4990) {
return 2;
} else if (longSide > 4990 && longSide < 10240) {
return 4;
} else {
return longSide / 1280 == 0 ? 1 : longSide / 1280;
}
} else if (scale <= 0.5625 && scale > 0.5) {
return longSide / 1280 == 0 ? 1 : longSide / 1280;
} else {
return (int) Math.ceil(longSide / (1280.0 / scale));
}
}
//旋转图片到合适角度
private Bitmap rotatingImage(File fromFile, Bitmap bitmap) throws IOException {
ExifInterface srcExif;
srcExif = new ExifInterface(new FileInputStream(fromFile));
if (srcExif == null) {
return bitmap;
}
Matrix matrix = new Matrix();
int angle = 0;
int orientation = srcExif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
angle = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
angle = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
angle = 270;
break;
}
matrix.postRotate(angle);
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
//给新产生的图片生成一个路径
private String makeName(Context context) {
File cacheDir = null;
try {
cacheDir = context.getExternalCacheDir();
} catch (Exception e) {
Log.e(LOGS, "error : " + e.getMessage());
}
if (cacheDir != null) {
File result = new File(cacheDir, FIELPOSITION);
if (!result.mkdirs() && (!result.exists() || !result.isDirectory())) {
return null;
}
String cacheBuilder = "/" + System.currentTimeMillis() + "/" + (int) (Math.random() * 100) + ".jpg";
return result.getAbsolutePath() + cacheBuilder;
}
Log.e(LOGS, "can't make a file position!");
return null;
}
public interface OnCompress {
void compressBitmapSuccess(Bitmap bitmap);
void compressFileSuccess(File file);
void compressFail(Throwable message);
}
}
最后还是要感谢以下文章提供的资源,就不一一感谢了,贴出参考文章:
详解Bitmap尺寸压缩与质量压缩
Bitmap类、BitmapFactory及BitmapFactory类中的常用方法
Android学习记录(9)—Android之Matrix的用法
Android矩阵(Matrix)简单使用
Android相机拍照方向旋转的解决方案:ExifInterface
Android Bitmap压缩图像的正确方法(compress的误区)
好了,今天学习就到这,咱们下回再见。。。
网友评论