开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情
1 概述
Bitmap是Android系统常用处理图片的类
2 Android图片大小
2.1 文件大小
file.length
2.2 流的大小
byte数:bitmap.getByteCount()
2.3 bitmap所占内存大小
Bitmap所占内存 = width * height * 一个像素点占用的字节数
Bitmap所占的内存大小根据其宽度、高度和配置(像素格式)决定。计算公式是:Bitmap内存 = 宽度(px)* 高度(px)* 每像素所用字节数。
每像素所用字节数取决于Bitmap.Config:
- ARGB_8888:每像素占4字节
- RGB_565 或 ARGB_4444:每像素占2字节
- ALPHA_8:每像素占1字节
例如:上述12642780的图片所占内存为:12642780*4=14,055,680 与上图所示约相等
3 Android图片压缩
3.1 质量压缩
原理:通过返回的位图具有不同的bitdepth、或丢失每个像素的alpha(如,JPEG仅支持不透明像素)
用途:一般用于上传大图前的处理,节省流量,用于压缩file的大小,内存不会变
压缩方法
/**
* Write a compressed version of the bitmap to the specified outputstream.
* If this returns true, the bitmap can be reconstructed by passing a
* corresponding inputstream to BitmapFactory.decodeStream(). Note: not
* all Formats support all bitmap configs directly, so it is possible that
* the returned bitmap from BitmapFactory could be in a different bitdepth,
* and/or may have lost per-pixel alpha (e.g. JPEG only supports opaque
* pixels).
*
* @param format The format of the compressed image
* @param quality Hint to the compressor, 0-100. The value is interpreted
* differently depending on the {@link CompressFormat}.
* @param stream The outputstream to write the compressed data.
* @return true if successfully compressed to the specified stream.
*/
@WorkerThread
public boolean compress(CompressFormat format, int quality, OutputStream stream) {
checkRecycled("Can't compress a recycled bitmap");
// do explicit check before calling the native method
if (stream == null) {
throw new NullPointerException();
}
if (quality < 0 || quality > 100) {
throw new IllegalArgumentException("quality must be 0..100");
}
StrictMode.noteSlowCall("Compression of a bitmap is slow");
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "Bitmap.compress");
boolean result = nativeCompress(mNativePtr, format.nativeInt,
quality, stream, new byte[WORKING_COMPRESS_STORAGE]);
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
return result;
}
压缩格式
/**
* Specifies the known formats a bitmap can be compressed into
*/
public enum CompressFormat {
/**
* Compress to the JPEG format. {@code quality} of {@code 0} means
* compress for the smallest size. {@code 100} means compress for max
* visual quality.
*/
JPEG (0),
/**
* Compress to the PNG format. PNG is lossless, so {@code quality} is
* ignored.
*/
// PNG图片是无损图片格式,其质量不能被降低,会忽略该参数
PNG (1),
/**
* Compress to the WEBP format. {@code quality} of {@code 0} means
* compress for the smallest size. {@code 100} means compress for max
* visual quality. As of {@link android.os.Build.VERSION_CODES#Q}, a
* value of {@code 100} results in a file in the lossless WEBP format.
* Otherwise the file will be in the lossy WEBP format.
*
* @deprecated in favor of the more explicit
* {@link CompressFormat#WEBP_LOSSY} and
* {@link CompressFormat#WEBP_LOSSLESS}.
*/
@Deprecated
WEBP (2),
/**
* Compress to the WEBP lossy format. {@code quality} of {@code 0} means
* compress for the smallest size. {@code 100} means compress for max
* visual quality.
*/
WEBP_LOSSY (3),
/**
* Compress to the WEBP lossless format. {@code quality} refers to how
* much effort to put into compression. A value of {@code 0} means to
* compress quickly, resulting in a relatively large file size.
* {@code 100} means to spend more time compressing, resulting in a
* smaller file.
*/
WEBP_LOSSLESS (4);
CompressFormat(int nativeInt) {
this.nativeInt = nativeInt;
}
final int nativeInt;
}
实现
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int options = 80;
bmp.compress(Bitmap.CompressFormat.JPEG, options, baos);
while (baos.toByteArray().length / 1024 > 100) {
baos.reset();
options -= 10;
bmp.compress(Bitmap.CompressFormat.JPEG, options, baos);
}
try {
FileOutputStream fos = new FileOutputStream(file);
fos.write(baos.toByteArray());
fos.flush();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
3.2 尺寸压缩
用途:用于生成缩略图,减少Bitmap的像素,压缩bitmap在内存中的大小
压缩方法
/**
* Returns a bitmap from subset of the source bitmap,
* transformed by the optional matrix. The new bitmap may be the
* same object as source, or a copy may have been made. It is
* initialized with the same density and color space as the original
* bitmap.
*
* If the source bitmap is immutable and the requested subset is the
* same as the source bitmap itself, then the source bitmap is
* returned and no new bitmap is created.
*
* The returned bitmap will always be mutable except in the following scenarios:
* (1) In situations where the source bitmap is returned and the source bitmap is immutable
*
* (2) The source bitmap is a hardware bitmap. That is {@link #getConfig()} is equivalent to
* {@link Config#HARDWARE}
*
* @param source The bitmap we are subsetting
* @param x The x coordinate of the first pixel in source
* @param y The y coordinate of the first pixel in source
* @param width The number of pixels in each row
* @param height The number of rows
* @param m Optional matrix to be applied to the pixels
* @param filter true if the source should be filtered.
* Only applies if the matrix contains more than just
* translation.
* @return A bitmap that represents the specified subset of source
* @throws IllegalArgumentException if the x, y, width, height values are
* outside of the dimensions of the source bitmap, or width is <= 0,
* or height is <= 0, or if the source bitmap has already been recycled
*/
public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height,
@Nullable Matrix m, boolean filter) {
实现
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = false
inSampleSize = 3
inPreferredConfig = Bitmap.Config.RGB_565
}
val srcBitmap = BitmapFactory.decodeFile(srcPath, options)
val bitmapWidth = srcBitmap.width
val bitmapHeight = srcBitmap.height
val reqWidth = ScreenUtils.getScreenWidth(BaseApplication.getInstance())
val reqHeight = ScreenUtils.getScreenHeight(BaseApplication.getInstance())
val scaleWidth = reqWidth.toFloat() / bitmapWidth.toFloat()
val scaleHeight = reqHeight.toFloat() / bitmapHeight.toFloat()
val matrix = Matrix()
matrix.postScale(scaleWidth, scaleHeight)
val scaleBitmap =
Bitmap.createBitmap(srcBitmap, 0, 0, bitmapWidth, bitmapHeight, matrix, false)
- 通过inSampleSize设置采样率缩放法
由于采样率是整数,大概率不能得到精确的宽、高值,故此方法不是很建议
BitmapFactory.Options newOpts = new BitmapFactory.Options();
newOpts.inJustDecodeBounds = true;//只读边,不读内容
Bitmap bitmap = BitmapFactory.decodeFile(srcPath, newOpts);
newOpts.inJustDecodeBounds = false;
int w = newOpts.outWidth;
int h = newOpts.outHeight;
float hh = 800f;//
float ww = 480f;//
int be = 1;
if (w > h && w > ww) {
be = (int) (newOpts.outWidth / ww);
} else if (w < h && h > hh) {
be = (int) (newOpts.outHeight / hh);
}
if (be <= 0)
be = 1;
newOpts.inSampleSize = be;//设置采样率
newOpts.inPreferredConfig = Config.ARGB_8888;//该模式是默认的,可不设
newOpts.inPurgeable = true;// 同时设置才会有效
newOpts.inInputShareable = true;//。当系统内存不够时候图片自动被回收
bitmap = BitmapFactory.decodeFile(srcPath, newOpts);
return bitmap;
3.3 不同压缩格式的压缩率
webp > jpeg > png
/**
* Specifies the known formats a bitmap can be compressed into
*/
public enum CompressFormat {
/**
* Compress to the JPEG format. {@code quality} of {@code 0} means
* compress for the smallest size. {@code 100} means compress for max
* visual quality.
*/
JPEG (0),
/**
* Compress to the PNG format. PNG is lossless, so {@code quality} is
* ignored.
*/
PNG (1),
/**
* Compress to the WEBP format. {@code quality} of {@code 0} means
* compress for the smallest size. {@code 100} means compress for max
* visual quality. As of {@link android.os.Build.VERSION_CODES#Q}, a
* value of {@code 100} results in a file in the lossless WEBP format.
* Otherwise the file will be in the lossy WEBP format.
*
* @deprecated in favor of the more explicit
* {@link CompressFormat#WEBP_LOSSY} and
* {@link CompressFormat#WEBP_LOSSLESS}.
*/
@Deprecated
WEBP (2),
/**
* Compress to the WEBP lossy format. {@code quality} of {@code 0} means
* compress for the smallest size. {@code 100} means compress for max
* visual quality.
*/
WEBP_LOSSY (3),
/**
* Compress to the WEBP lossless format. {@code quality} refers to how
* much effort to put into compression. A value of {@code 0} means to
* compress quickly, resulting in a relatively large file size.
* {@code 100} means to spend more time compressing, resulting in a
* smaller file.
*/
WEBP_LOSSLESS (4);
CompressFormat(int nativeInt) {
this.nativeInt = nativeInt;
}
final int nativeInt;
}
3.4 编码格式
ALPHA_8
表示8位Alpha位图,即A=8,一个像素点占用1个字节,它没有颜色,只有透明度
ARGB_4444
表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素点占4+4+4+4=16位,2个字节
ARGB_8888
表示32位ARGB位图,即A=8,R=8,G=8,B=8,一个像素点占8+8+8+8=32位,4个字节
RGB_565
表示16位RGB位图,即R=5,G=6,B=5,它没有透明度,一个像素点占5+6+5=16位,2个字节
BitmapFactory.Options
in开头的代表的就是设置某某参数;out开头的代表的就是获取某某参数
3.5 高质量绘制
// 不使用采样解码,直接解码原图
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = false
inPreferredConfig = Bitmap.Config.ARGB_8888
setPreferredColorSpace(this)
}
val originBitmap = BitmapFactory.decodeFile(srcPath, options) ?: return false
// 创建目标Bitmap(屏幕尺寸)
val screenWidth = ScreenUtils.getScreenWidth()
val screenHeight = ScreenUtils.getScreenHeight()
val cropBitmap = Bitmap.createBitmap(
screenWidth,
screenHeight,
Bitmap.Config.ARGB_8888
)
try {
val canvas = Canvas(cropBitmap)
// 设置抗锯齿和双线性过滤
canvas.drawFilter = PaintFlagsDrawFilter(
0, Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG
)
// 获取原图尺寸(考虑旋转后的尺寸)
val bitmapWidth: Float
val bitmapHeight: Float
if (item.orientaion == 90 || item.orientaion == 270) {
// 旋转90度或270度时,宽高互换
bitmapWidth = originBitmap.height.toFloat()
bitmapHeight = originBitmap.width.toFloat()
} else {
bitmapWidth = originBitmap.width.toFloat()
bitmapHeight = originBitmap.height.toFloat()
}
// 计算CenterCrop的缩放比例(使图片完全覆盖屏幕)
val scaleX = screenWidth / bitmapWidth
val scaleY = screenHeight / bitmapHeight
val scale = Math.max(scaleX, scaleY)
// 计算缩放后的图片尺寸
val scaledWidth = bitmapWidth * scale
val scaledHeight = bitmapHeight * scale
// 计算居中偏移
val dx = (screenWidth - scaledWidth) / 2f
val dy = (screenHeight - scaledHeight) / 2f
// 构建Matrix:旋转 -> 缩放 -> 平移
val matrix = Matrix()
// 1. 先旋转(围绕原图中心)
if (item.orientaion != 0) {
// 将图片移动到原点,旋转,再移回
matrix.postTranslate(-originBitmap.width / 2f, -originBitmap.height / 2f)
matrix.postRotate(item.orientaion.toFloat())
matrix.postTranslate(originBitmap.width / 2f, originBitmap.height / 2f)
}
// 2. 缩放(以原点为中心)
matrix.postScale(scale, scale)
// 3. 平移到居中位置
matrix.postTranslate(dx, dy)
// 使用高质量渲染
val paint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
paint.isFilterBitmap = true
canvas.drawBitmap(originBitmap, matrix, paint)
originBitmap.recycle()
BitmapUtils.saveHighQuantityBitmap(cropBitmap, desF)
} finally {
if (!cropBitmap.isRecycled) {
cropBitmap.recycle()
}
}
保存时跟时间戳绑定:
// 处理文件名冲突,删除旧文件并生成新的唯一文件名
val fileNameWithoutExtension = fileName.substringBeforeLast(".")
val fileExtension = fileName.substringAfterLast(".", Constant.PPIC_FILE_EXTENSION)
// 删除所有匹配前缀的旧文件(包括原始文件和带时间戳的历史文件)
// 情况A:原始文件名,如 image.jpg
// 情况B:带时间戳的文件名,如 image_1234567890.jpg
dir.listFiles()?.forEach { file ->
val name = file.name
// 匹配规则:
// 1. 完全匹配原始文件名(情况A)
// 2. 匹配 fileNameWithoutExtension_数字.fileExtension 格式(情况B)
val matchesPattern = name == fileName ||
(name.startsWith("${fileNameWithoutExtension}_") &&
name.endsWith(".${fileExtension}") &&
name.substring(fileNameWithoutExtension.length + 1, name.length - fileExtension.length - 1).all { it.isDigit() })
if (matchesPattern) {
val deleted = file.delete()
PictorialLog.i(TAG, "delete old file: ${file.absolutePath}, success: $deleted")
}
}
// 生成带时间戳的新文件名
val timestamp = System.currentTimeMillis()
fileName = "${fileNameWithoutExtension}_${timestamp}.${fileExtension}"
PictorialLog.i(TAG, "generate new filename with timestamp: $fileName")
// 使用新文件名创建目标文件对象
val desF = File(desDir + File.separator + fileName)