【基础篇2】关于Bitmap的基础操作

140 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 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:

  1. ARGB_8888:每像素占4字节
  2. RGB_565 或 ARGB_4444:每像素占2字节
  3. ALPHA_8:每像素占1字节

image.png 例如:上述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)
  1. 通过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)