Android 图像编辑实战指南:从基础操作到进阶效果

在移动应用中,图像编辑功能已成为标配 —— 社交 APP 需要裁剪头像,电商 APP 需要给商品图加水印,工具 APP 需要提供滤镜效果。看似简单的 “裁剪”“缩放” 背后,实则涉及 Bitmap 像素操作、内存管理、性能优化等核心技术。很多开发者在实现时会遇到 “编辑后图片模糊”“操作时卡顿”“大图片编辑 OOM” 等问题,根源在于对图像编辑的底层逻辑理解不足。

本文将从实际开发需求出发,系统讲解 Android 图像编辑的核心技术:从基础的裁剪、缩放、旋转,到进阶的滤镜、水印、圆角处理,每个功能都提供完整实现代码和优化方案,帮你避开常见陷阱,实现高效、高质量的图像编辑功能。

一、图像编辑的基础:Bitmap 像素操作原理

所有图像编辑功能的底层都是对 Bitmap 像素的操作。Bitmap 是 Android 中唯一能直接操作像素的图像格式,其本质是 “内存中的像素矩阵”—— 每个像素的颜色(ARGB 值)决定了图像的显示效果。

1.1 Bitmap 的像素存储与颜色表示

  • 像素存储:Bitmap 的像素按行存储在内存中,例如 100x100 的 Bitmap 有 10000 个像素,每个像素占用 2-4 字节(取决于像素格式);
  • 颜色表示:每个像素用 ARGB 值表示(Alpha 透明度、Red 红色、Green 绿色、Blue 蓝色),例如0xFFFF0000表示不透明的红色(A=0xFF,R=0xFF,G=0x00,B=0x00);
  • 像素格式
  • ARGB_8888:每个像素 4 字节(最高质量,支持透明),1080x1920 的 Bitmap 约占用 8MB 内存;
  • RGB_565:每个像素 2 字节(无透明通道),内存占用仅为 ARGB_8888 的一半,适合非透明图像。

关键结论:图像编辑时,应根据需求选择像素格式(非透明图像用 RGB_565 节省内存),并始终控制 Bitmap 的大小(避免加载超过编辑所需的大图片)。

1.2 图像编辑的核心步骤

无论何种编辑操作,基本流程都可概括为:

1.加载原图:将图像(File/Uri/Drawable)转为可编辑的 Bitmap(需控制大小,避免 OOM);

2.创建编辑后的 Bitmap:根据编辑需求创建新的 Bitmap(如裁剪后的尺寸、旋转后的尺寸);

3.像素操作:通过 Canvas 绘制或直接修改像素数组,实现编辑效果;

4.保存结果:将编辑后的 Bitmap 转为目标格式(如保存为 File、显示为 Drawable);

5.释放资源:回收原图 Bitmap,避免内存泄漏。

这个流程的核心是 “尽量减少中间 Bitmap 的创建” 和 “及时释放不再使用的 Bitmap”,这是避免编辑时卡顿和 OOM 的关键。

二、基础编辑功能:裁剪、缩放、旋转

基础编辑功能是所有图像编辑需求的基石,实现时需兼顾 “精度” 和 “性能”—— 既要保证编辑后的图像清晰,又要避免操作时卡顿。

2.1 图像裁剪:保留指定区域

裁剪是最常用的编辑功能(如裁剪头像、截取图片中的部分内容),核心是 “从原图中截取指定矩形区域的像素”。

(1)基本裁剪实现(按坐标裁剪)
/**
 * 裁剪Bitmap的指定区域
 * @param srcBitmap 原图Bitmap
 * @param x 裁剪区域左上角x坐标(相对于原图)
 * @param y 裁剪区域左上角y坐标(相对于原图)
 * @param width 裁剪区域宽度
 * @param height 裁剪区域高度
 * @return 裁剪后的Bitmap(null表示裁剪失败)
 */
public static Bitmap cropBitmap(Bitmap srcBitmap, int x, int y, int width, int height) {
    if (srcBitmap == null) return null;

    // 检查裁剪区域是否在原图范围内(避免越界)
    if (x < 0 || y < 0 || width <= 0 || height <= 0
            || x + width > srcBitmap.getWidth()
            || y + height > srcBitmap.getHeight()) {
        return null;
    }

    try {
        // 从原图裁剪指定区域(创建新Bitmap,像素从原图复制)
        return Bitmap.createBitmap(
                srcBitmap,
                x, y, // 起始坐标
                width, height // 裁剪宽高
        );
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
        return null;
    }
}

使用场景:从图片中心裁剪正方形区域(如头像裁剪):

// 原图
Bitmap originalBitmap = BitmapFactory.decodeFile("/sdcard/image.jpg");
if (originalBitmap == null) return;

// 计算裁剪区域(从中心裁剪200x200的正方形)
int srcWidth = originalBitmap.getWidth();
int srcHeight = originalBitmap.getHeight();
int cropSize = Math.min(srcWidth, srcHeight); // 取宽高中的较小值
int x = (srcWidth - cropSize) / 2; // 居中x坐标
int y = (srcHeight - cropSize) / 2; // 居中y坐标

// 裁剪
Bitmap croppedBitmap = cropBitmap(originalBitmap, x, y, cropSize, cropSize);
// 显示裁剪结果
imageView.setImageBitmap(croppedBitmap);

// 回收原图(不再使用)
originalBitmap.recycle();
(2)优化:避免裁剪后图片过大

若原图尺寸远大于显示需求(如 4000x3000 的图片裁剪后仍有 2000x2000),需在裁剪后进一步缩放:

/**
 * 裁剪并缩放(适合大图片裁剪)
 * @param srcBitmap 原图
 * @param x 裁剪x
 * @param y 裁剪y
 * @param cropWidth 裁剪宽度
 * @param cropHeight 裁剪高度
 * @param targetWidth 最终目标宽度
 * @param targetHeight 最终目标高度
 * @return 裁剪并缩放后的Bitmap
 */
public static Bitmap cropAndScale(Bitmap srcBitmap, int x, int y, int cropWidth, int cropHeight, int targetWidth, int targetHeight) {
    // 先裁剪
    Bitmap cropped = cropBitmap(srcBitmap, x, y, cropWidth, cropHeight);
    if (cropped == null) return null;

    // 再缩放(若裁剪后的尺寸大于目标尺寸)
    if (cropped.getWidth() > targetWidth || cropped.getHeight() > targetHeight) {
        Bitmap scaled = scaleBitmap(cropped, targetWidth, targetHeight);
        cropped.recycle(); // 回收裁剪后的临时Bitmap
        return scaled;
    }

    return cropped;
}

使用场景:裁剪头像并限制最大尺寸为 200x200:

Bitmap croppedAndScaled = cropAndScale(originalBitmap, x, y, cropSize, cropSize, 200, 200);

2.2 图像缩放:按比例调整大小

缩放用于 “放大图片细节” 或 “缩小图片尺寸”(如将大图压缩为缩略图),需注意保持宽高比以避免图片拉伸。

(1)按目标尺寸缩放(保持宽高比)
/**
 * 缩放Bitmap到目标尺寸(保持宽高比,避免拉伸)
 * @param srcBitmap 原图
 * @param targetWidth 目标宽度
 * @param targetHeight 目标高度
 * @return 缩放后的Bitmap
 */
public static Bitmap scaleBitmap(Bitmap srcBitmap, int targetWidth, int targetHeight) {
    if (srcBitmap == null) return null;

    // 计算缩放比例(取宽高比例中的较小值,避免超出目标尺寸)
    float scaleX = (float) targetWidth / srcBitmap.getWidth();
    float scaleY = (float) targetHeight / srcBitmap.getHeight();
    float scale = Math.min(scaleX, scaleY); // 保持宽高比

    // 计算缩放后的实际尺寸
    int scaledWidth = (int) (srcBitmap.getWidth() * scale);
    int scaledHeight = (int) (srcBitmap.getHeight() * scale);

    // 缩放Bitmap(使用抗锯齿避免模糊)
    return Bitmap.createScaledBitmap(
            srcBitmap,
            scaledWidth,
            scaledHeight,
            true // 抗锯齿
    );
}

优势:通过计算最小缩放比例,确保缩放后的图片能完整放入目标尺寸,且不拉伸。例如:将 1000x500 的图片缩放到 300x300 的目标尺寸,会按 0.3 的比例缩放到 300x150(保持 2:1 的宽高比)。

(2)按缩放比例缩放(如放大 1.5 倍)
/**
 * 按比例缩放(如0.5f缩小一半,2.0f放大一倍)
 * @param srcBitmap 原图
 * @param scale 缩放比例(>1放大,<1缩小)
 * @return 缩放后的Bitmap
 */
public static Bitmap scaleBitmapByRatio(Bitmap srcBitmap, float scale) {
    if (srcBitmap == null || scale <= 0) return null;

    int newWidth = (int) (srcBitmap.getWidth() * scale);
    int newHeight = (int) (srcBitmap.getHeight() * scale);

    return Bitmap.createScaledBitmap(srcBitmap, newWidth, newHeight, true);
}

使用场景:放大图片细节(如查看图片局部):

// 放大1.5倍
Bitmap enlarged = scaleBitmapByRatio(originalBitmap, 1.5f);

2.3 图像旋转:修正方向与角度

旋转用于 “修正图片方向”(如相机拍摄的图片旋转 90 度)或 “实现特殊效果”(如旋转 180 度翻转图片)。

(1)按指定角度旋转
/**
 * 旋转Bitmap指定角度
 * @param srcBitmap 原图
 * @param degrees 旋转角度(正为顺时针,负为逆时针,如90、180、-90)
 * @return 旋转后的Bitmap
 */
public static Bitmap rotateBitmap(Bitmap srcBitmap, float degrees) {
    if (srcBitmap == null || degrees % 360 == 0) {
        return srcBitmap; // 0度旋转直接返回原图
    }

    // 创建旋转矩阵
    Matrix matrix = new Matrix();
    matrix.postRotate(degrees);

    // 根据矩阵旋转Bitmap(使用抗锯齿)
    return Bitmap.createBitmap(
            srcBitmap,
            0, 0,
            srcBitmap.getWidth(),
            srcBitmap.getHeight(),
            matrix,
            true // 抗锯齿
    );
}

使用场景:修正相机拍摄图片的旋转问题(结合 EXIF 信息):

// 读取图片的EXIF旋转角度(参考前文fixBitmapRotation方法)
int exifRotation = getExifRotation(imagePath); // 如90度
// 旋转图片
Bitmap rotated = rotateBitmap(originalBitmap, exifRotation);
(2)旋转后的内存优化

旋转可能导致 Bitmap 尺寸变大(如正方形旋转为菱形后,边界扩大),需根据需求裁剪多余空白:

/**
 * 旋转并裁剪空白区域(适合正方形旋转为菱形后去除空白)
 * @param srcBitmap 原图
 * @param degrees 旋转角度
 * @return 旋转并裁剪后的Bitmap
 */
public static Bitmap rotateAndCrop(Bitmap srcBitmap, float degrees) {
    Bitmap rotated = rotateBitmap(srcBitmap, degrees);
    if (rotated == null) return null;

    // 计算裁剪区域(去除旋转后的空白)
    int width = rotated.getWidth();
    int height = rotated.getHeight();
    int cropSize = Math.min(width, height);
    int x = (width - cropSize) / 2;
    int y = (height - cropSize) / 2;

    Bitmap cropped = cropBitmap(rotated, x, y, cropSize, cropSize);
    rotated.recycle();
    return cropped;
}

2.4 基础编辑的性能优化

基础编辑(尤其是裁剪、缩放)操作频繁创建 Bitmap,易导致内存问题,需注意:

1.及时回收临时 Bitmap:中间过程产生的 Bitmap(如裁剪后的临时图)使用后立即回收;

2.避免在主线程操作大图片:超过 1000x1000 的图片编辑需在子线程执行;

// 用Coroutine在子线程执行编辑
CoroutineScope(Dispatchers.IO).launch {
    Bitmap edited = cropBitmap(originalBitmap, x, y, width, height);
    withContext(Dispatchers.Main) {
        imageView.setImageBitmap(edited);
    }
}

3.优先使用createScaledBitmap而非Matrix缩放:前者性能更优(底层有优化);

4.大图片先缩小再编辑:对 4000x3000 的图片,先缩放到 1000x750 再裁剪,可减少 90% 的内存占用。

三、进阶编辑功能:滤镜、水印、圆角

进阶编辑功能能提升图像的视觉效果,实现时需平衡 “效果质量” 和 “性能开销”—— 复杂的滤镜效果若实现不当,会导致操作时严重卡顿。

3.1 图像滤镜:调整颜色与风格

滤镜通过修改像素的 ARGB 值实现特殊效果(如黑白、怀旧、高亮),核心是 “遍历像素并计算新颜色”。

(1)黑白滤镜(基础滤镜)

黑白滤镜的原理是 “将每个像素的 RGB 值转为灰度值”(灰度 = 0.299R + 0.587G + 0.114*B)。

/**
 * 黑白滤镜(将彩色图转为黑白图)
 * @param srcBitmap 原图
 * @return 黑白效果的Bitmap
 */
public static Bitmap applyBlackWhiteFilter(Bitmap srcBitmap) {
    if (srcBitmap == null) return null;

    // 创建可修改的Bitmap(原图可能是不可变的)
    Bitmap destBitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);
    int width = destBitmap.getWidth();
    int height = destBitmap.getHeight();

    // 遍历所有像素
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            // 获取当前像素的ARGB值
            int pixel = destBitmap.getPixel(x, y);
            int a = Color.alpha(pixel); // 透明度
            int r = Color.red(pixel);   // 红色
            int g = Color.green(pixel); // 绿色
            int b = Color.blue(pixel);  // 蓝色

            // 计算灰度值(黑白滤镜核心)
            int gray = (int) (0.299 * r + 0.587 * g + 0.114 * b);

            // 设置新像素(灰度值赋予RGB,保持透明度)
            int newPixel = Color.argb(a, gray, gray, gray);
            destBitmap.setPixel(x, y, newPixel);
        }
    }

    return destBitmap;
}

优化技巧:使用getPixels批量获取像素(比getPixel逐个获取快 10 倍以上):

// 优化版:批量处理像素
public static Bitmap applyBlackWhiteFilterOptimized(Bitmap srcBitmap) {
    if (srcBitmap == null) return null;

    Bitmap destBitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);
    int width = destBitmap.getWidth();
    int height = destBitmap.getHeight();
    int totalPixels = width * height;

    // 批量获取像素数组
    int[] pixels = new int[totalPixels];
    destBitmap.getPixels(pixels, 0, width, 0, 0, width, height);

    // 遍历像素数组(比逐个getPixel快得多)
    for (int i = 0; i < totalPixels; i++) {
        int pixel = pixels[i];
        int a = Color.alpha(pixel);
        int r = Color.red(pixel);
        int g = Color.green(pixel);
        int b = Color.blue(pixel);

        int gray = (int) (0.299 * r + 0.587 * g + 0.114 * b);
        pixels[i] = Color.argb(a, gray, gray, gray);
    }

    // 将处理后的像素数组设置回Bitmap
    destBitmap.setPixels(pixels, 0, width, 0, 0, width, height);
    return destBitmap;
}
(2)怀旧滤镜(色彩调整)

怀旧滤镜通过降低蓝色分量、提高红色和绿色分量,模拟老照片效果:

/**
 * 怀旧滤镜
 * @param srcBitmap 原图
 * @return 怀旧效果的Bitmap
 */
public static Bitmap applyNostalgiaFilter(Bitmap srcBitmap) {
    if (srcBitmap == null) return null;

    Bitmap destBitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);
    int width = destBitmap.getWidth();
    int height = destBitmap.getHeight();
    int[] pixels = new int[width * height];
    destBitmap.getPixels(pixels, 0, width, 0, 0, width, height);

    for (int i = 0; i < pixels.length; i++) {
        int pixel = pixels[i];
        int a = Color.alpha(pixel);
        int r = Color.red(pixel);
        int g = Color.green(pixel);
        int b = Color.blue(pixel);

        // 怀旧效果算法:降低蓝色,提高红绿色
        int newR = (int) (0.393 * r + 0.769 * g + 0.189 * b);
        int newG = (int) (0.349 * r + 0.686 * g + 0.168 * b);
        int newB = (int) (0.272 * r + 0.534 * g + 0.131 * b);

        // 确保颜色值在0-255范围内
        newR = Math.min(255, newR);
        newG = Math.min(255, newG);
        newB = Math.min(255, newB);

        pixels[i] = Color.argb(a, newR, newG, newB);
    }

    destBitmap.setPixels(pixels, 0, width, 0, 0, width, height);
    return destBitmap;
}
(3)使用 RenderScript 加速滤镜(大图片必备)

遍历像素的滤镜在大图片(如 2000x2000)上会非常慢(可能超过 1 秒)。RenderScript 是 Android 提供的高性能计算框架,可通过 GPU 加速像素处理,速度比 Java 遍历快 5-10 倍。

黑白滤镜的 RenderScript 实现

/**
 * 使用RenderScript实现黑白滤镜(高性能)
 * @param context 上下文
 * @param srcBitmap 原图
 * @return 黑白效果Bitmap
 */
public static Bitmap applyBlackWhiteWithRenderScript(Context context, Bitmap srcBitmap) {
    if (srcBitmap == null) return null;

    // 创建输出Bitmap
    Bitmap destBitmap = Bitmap.createBitmap(
            srcBitmap.getWidth(),
            srcBitmap.getHeight(),
            srcBitmap.getConfig()
    );

    // 初始化RenderScript
    RenderScript rs = RenderScript.create(context);
    // 创建输入输出分配器
    Allocation input = Allocation.createFromBitmap(rs, srcBitmap);
    Allocation output = Allocation.createFromBitmap(rs, destBitmap);

    // 创建黑白滤镜脚本(需在res/raw下创建bw_filter.rs)
    ScriptIntrinsicColorMatrix colorMatrix = ScriptIntrinsicColorMatrix.create(rs);
    // 设置黑白矩阵(RGB转为灰度)
    Matrix3f matrix = new Matrix3f();
    matrix.set(0, 0, 0.299f);
    matrix.set(0, 1, 0.587f);
    matrix.set(0, 2, 0.114f);
    matrix.set(1, 0, 0.299f);
    matrix.set(1, 1, 0.587f);
    matrix.set(1, 2, 0.114f);
    matrix.set(2, 0, 0.299f);
    matrix.set(2, 1, 0.587f);
    matrix.set(2, 2, 0.114f);
    colorMatrix.setColorMatrix(matrix);

    // 执行滤镜
    colorMatrix.forEach(input, output);
    // 将结果复制到输出Bitmap
    output.copyTo(destBitmap);

    // 释放资源
    input.destroy();
    output.destroy();
    colorMatrix.destroy();
    rs.destroy();

    return destBitmap;
}

RenderScript 脚本(res/raw/bw_filter.rs):无需额外代码,直接使用ScriptIntrinsicColorMatrix内置功能。

优势:2000x2000 的图片,Java 遍历需 1.2 秒,RenderScript 仅需 0.15 秒,性能提升 8 倍。

3.2 图像水印:添加文字或图片标识

水印用于 “版权声明”(如图片添加 APP 名称)或 “信息补充”(如拍摄时间、地点),分为文字水印和图片水印。

(1)文字水印(指定位置和样式)
/**
 * 给Bitmap添加文字水印
 * @param srcBitmap 原图
 * @param text 水印文字(如"我的APP")
 * @param textSize 文字大小(sp)
 * @param textColor 文字颜色
 * @param position 水印位置(1=左上,2=右上,3=左下,4=右下)
 * @param padding 边距(dp)
 * @return 添加水印后的Bitmap
 */
public static Bitmap addTextWatermark(Bitmap srcBitmap, String text, float textSize, int textColor, int position, int padding) {
    if (srcBitmap == null || TextUtils.isEmpty(text)) return srcBitmap;

    // 创建可绘制的Bitmap
    Bitmap destBitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);
    Canvas canvas = new Canvas(destBitmap); // 创建画布

    // 转换单位(sp→px,dp→px)
    Resources resources = Resources.getSystem();
    float textSizePx = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_SP,
            textSize,
            resources.getDisplayMetrics()
    );
    int paddingPx = (int) TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            padding,
            resources.getDisplayMetrics()
    );

    // 配置画笔
    Paint paint = new Paint();
    paint.setColor(textColor);
    paint.setTextSize(textSizePx);
    paint.setAntiAlias(true); // 抗锯齿
    paint.setAlpha(128); // 半透明(0-255)
    paint.setTextAlign(Paint.Align.LEFT);

    // 计算文字宽高
    Rect textBounds = new Rect();
    paint.getTextBounds(text, 0, text.length(), textBounds);
    int textWidth = textBounds.width();
    int textHeight = textBounds.height();

    // 计算水印位置坐标
    int x = 0, y = 0;
    int bitmapWidth = destBitmap.getWidth();
    int bitmapHeight = destBitmap.getHeight();

    switch (position) {
        case 1: // 左上
            x = paddingPx;
            y = paddingPx + textHeight; // y是基线位置,需加上文字高度
            break;
        case 2: // 右上
            x = bitmapWidth - paddingPx - textWidth;
            y = paddingPx + textHeight;
            break;
        case 3: // 左下
            x = paddingPx;
            y = bitmapHeight - paddingPx;
            break;
        case 4: // 右下
            x = bitmapWidth - paddingPx - textWidth;
            y = bitmapHeight - paddingPx;
            break;
    }

    // 绘制文字
    canvas.drawText(text, x, y, paint);

    return destBitmap;
}

使用场景:给图片右下角添加半透明水印:

Bitmap watermarked = addTextWatermark(
        originalBitmap,
        "我的APP",
        14, // 14sp
        Color.argb(128, 255, 255, 255), // 白色半透明
        4, // 右下
        16 // 16dp边距
);
(2)图片水印(添加 Logo)
/**
 * 给Bitmap添加图片水印(如Logo)
 * @param srcBitmap 原图
 * @param watermarkBitmap 水印图片(Logo)
 * @param position 位置(同文字水印)
 * @param padding 边距(dp)
 * @param alpha 透明度(0-255,0完全透明)
 * @return 添加水印后的Bitmap
 */
public static Bitmap addImageWatermark(Bitmap srcBitmap, Bitmap watermarkBitmap, int position, int padding, int alpha) {
    if (srcBitmap == null || watermarkBitmap == null) return srcBitmap;

    Bitmap destBitmap = srcBitmap.copy(Bitmap.Config.ARGB_8888, true);
    Canvas canvas = new Canvas(destBitmap);

    // 转换边距单位(dp→px)
    int paddingPx = (int) TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            padding,
            Resources.getSystem().getDisplayMetrics()
    );

    // 水印宽高
    int watermarkWidth = watermarkBitmap.getWidth();
    int watermarkHeight = watermarkBitmap.getHeight();
    // 原图宽高
    int srcWidth = srcBitmap.getWidth();
    int srcHeight = srcBitmap.getHeight();

    // 计算水印位置
    int x = 0, y = 0;
    switch (position) {
        case 1: // 左上
            x = paddingPx;
            y = paddingPx;
            break;
        case 2: // 右上
            x = srcWidth - paddingPx - watermarkWidth;
            y = paddingPx;
            break;
        case 3: // 左下
            x = paddingPx;
            y = srcHeight - paddingPx - watermarkHeight;
            break;
        case 4: // 右下
            x = srcWidth - paddingPx - watermarkWidth;
            y = srcHeight - paddingPx - watermarkHeight;
            break;
    }

    // 设置透明度
    Paint paint = new Paint();
    paint.setAlpha(alpha);

    // 绘制水印图片
    canvas.drawBitmap(watermarkBitmap, x, y, paint);

    return destBitmap;
}

使用场景:给图片添加右下角半透明 Logo:

// 加载Logo图片
Bitmap logo = BitmapFactory.decodeResource(getResources(), R.drawable.logo);
// 添加水印(透明度128=半透明)
Bitmap withLogo = addImageWatermark(originalBitmap, logo, 4, 16, 128);

3.3 圆角处理:给图片添加圆角或圆形效果

圆角图片用于 “头像”“卡片设计” 等场景,实现方式有两种:绘制圆角 Bitmap 或使用 Drawable(如RoundedBitmapDrawable)。

(1)绘制圆角 Bitmap
/**
 * 给Bitmap添加圆角
 * @param srcBitmap 原图
 * @param radius 圆角半径(dp)
 * @return 圆角Bitmap
 */
public static Bitmap createRoundCornerBitmap(Bitmap srcBitmap, float radius) {
    if (srcBitmap == null) return null;

    // 转换圆角半径单位(dp→px)
    radius = TypedValue.applyDimension(
            TypedValue.COMPLEX_UNIT_DIP,
            radius,
            Resources.getSystem().getDisplayMetrics()
    );

    // 创建输出Bitmap(ARGB_8888支持透明)
    Bitmap destBitmap = Bitmap.createBitmap(
            srcBitmap.getWidth(),
            srcBitmap.getHeight(),
            Bitmap.Config.ARGB_8888
    );
    Canvas canvas = new Canvas(destBitmap);

    // 绘制圆角矩形作为画布裁剪区域
    Paint paint = new Paint();
    paint.setAntiAlias(true); // 抗锯齿
    RectF rectF = new RectF(0, 0, srcBitmap.getWidth(), srcBitmap.getHeight());
    canvas.drawRoundRect(rectF, radius, radius, paint);

    // 设置画笔模式为“只绘制与已有内容重叠的区域”
    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));

    // 绘制原图(仅在圆角矩形区域内显示)
    canvas.drawBitmap(srcBitmap, 0, 0, paint);

    return destBitmap;
}

使用场景:将头像处理为 8dp 圆角:

Bitmap roundCorner = createRoundCornerBitmap(originalBitmap, 8);
(2)创建圆形 Bitmap(头像常用)

圆形是圆角的特殊情况(半径为宽高的一半):

/**
 * 创建圆形Bitmap(如圆形头像)
 * @param srcBitmap 原图(建议正方形)
 * @return 圆形Bitmap
 */
public static Bitmap createCircleBitmap(Bitmap srcBitmap) {
    if (srcBitmap == null) return null;

    // 取最小边作为圆形直径
    int size = Math.min(srcBitmap.getWidth(), srcBitmap.getHeight());
    // 裁剪为正方形(避免非正方形图片导致椭圆)
    Bitmap squareBitmap = cropBitmap(srcBitmap,
            (srcBitmap.getWidth() - size) / 2,
            (srcBitmap.getHeight() - size) / 2,
            size, size);

    // 绘制圆形
    Bitmap circleBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(circleBitmap);
    Paint paint = new Paint();
    paint.setAntiAlias(true);
    // 绘制圆形
    canvas.drawCircle(size / 2, size / 2, size / 2, paint);
    // 设置混合模式
    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    // 绘制正方形图片
    canvas.drawBitmap(squareBitmap, 0, 0, paint);

    squareBitmap.recycle(); // 回收裁剪的正方形Bitmap
    return circleBitmap;
}

优化方案:使用RoundedBitmapDrawable(无需创建新 Bitmap):

/**
 * 使用RoundedBitmapDrawable创建圆形图片(更高效)
 * @param context 上下文
 * @param srcBitmap 原图
 * @return RoundedBitmapDrawable(可直接设置给ImageView)
 */
public static RoundedBitmapDrawable createCircleDrawable(Context context, Bitmap srcBitmap) {
    if (srcBitmap == null) return null;

    // 裁剪为正方形
    int size = Math.min(srcBitmap.getWidth(), srcBitmap.getHeight());
    Bitmap squareBitmap = cropBitmap(srcBitmap,
            (srcBitmap.getWidth() - size) / 2,
            (srcBitmap.getHeight() - size) / 2,
            size, size);

    // 创建RoundedBitmapDrawable
    RoundedBitmapDrawable drawable = RoundedBitmapDrawableFactory.create(
            context.getResources(),
            squareBitmap
    );
    drawable.setCircular(true); // 设置为圆形
    drawable.setAntiAlias(true); // 抗锯齿

    return drawable;
}

// 使用:直接设置给ImageView,无需创建新Bitmap
RoundedBitmapDrawable circleDrawable = createCircleDrawable(this, originalBitmap);
imageView.setImageDrawable(circleDrawable);

优势:RoundedBitmapDrawable是 Drawable,不额外占用 Bitmap 内存,性能更优。

四、图像编辑的常见问题与解决方案

图像编辑涉及大量 Bitmap 操作,容易出现各种问题,以下是高频问题及解决办法。

4.1 编辑后图片模糊

原因

  • 缩放时未使用抗锯齿(createScaledBitmap的filter参数设为false);
  • 裁剪 / 缩放后图片尺寸过小(如将 1000x1000 的图片缩放到 50x50,再放大显示);
  • 像素格式选择错误(如用RGB_565显示需要透明的图片)。

解决方案

  • 缩放 / 旋转时始终开启抗锯齿(filter参数设为true);
  • 编辑后的图片尺寸不小于显示尺寸(如 ImageView 宽 200dp,则编辑后 Bitmap 宽不小于 200dp);
  • 透明图片用ARGB_8888格式,非透明图片用RGB_565。

4.2 编辑时卡顿或 ANR

原因

  • 在主线程处理大图片(如 2000x2000 的 Bitmap 滤镜操作);
  • 遍历像素时使用getPixel/setPixel(逐个操作效率极低);
  • 频繁创建和回收 Bitmap(导致 GC 频繁触发)。

解决方案

  • 所有编辑操作移到子线程(用 Coroutine 或 AsyncTask);
  • 批量操作像素(getPixels/setPixels)或使用 RenderScript;
  • 复用 Bitmap(如列表中的头像编辑,复用 convertView 的旧 Bitmap)。

4.3 编辑大图片时 OOM

原因

  • 加载原图时未缩放(直接加载 4000x3000 的图片,内存占用 48MB);
  • 编辑过程中创建多个临时 Bitmap(如裁剪→缩放→滤镜,每个步骤都创建新 Bitmap);
  • 未及时回收不再使用的 Bitmap(原图、临时图占用内存)。

解决方案

  • 加载原图时先缩放(如缩放到 1000x750 再编辑);
  • 合并编辑步骤(如裁剪和缩放合并为一个步骤,减少临时 Bitmap);
  • 编辑后立即回收原图和临时 Bitmap(bitmap.recycle() + bitmap = null)。

4.4 旋转后图片有黑色背景

原因:旋转非正方形图片时,Bitmap 尺寸扩大,新增区域默认填充黑色(透明通道未处理)。

解决方案

  • 使用ARGB_8888格式(支持透明);
  • 旋转后裁剪多余区域(如rotateAndCrop方法);
  • 绘制时设置画布背景为透明(canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR))。

五、图像编辑的最佳实践

结合前文内容,图像编辑的最佳实践可总结为以下原则:

1.内存优先

  • 加载图片时按编辑需求缩放(不加载大于需求的图片);
  • 及时回收中间 Bitmap(原图、临时处理结果);
  • 优先使用 Drawable(如RoundedBitmapDrawable)而非创建新 Bitmap。

2.性能优化

  • 大图片编辑用 RenderScript(GPU 加速);
  • 批量操作像素(避免getPixel逐个处理);
  • 子线程执行所有编辑操作,主线程只负责显示结果。

3.效果保证

  • 保持宽高比(避免图片拉伸);
  • 操作时开启抗锯齿(避免边缘锯齿);
  • 透明图片用ARGB_8888格式。

4.兼容性处理

  • Android 10+ 保存图片用MediaStore(替代直接操作 File);
  • 不同密度设备(如 hdpi、xxhdpi)适配单位(dp/sp 转 px)。

六、总结:图像编辑的核心逻辑

Android 图像编辑的本质是 “对 Bitmap 像素的可控修改”,无论是裁剪、滤镜还是水印,最终都落实到像素的选择、计算或替换。掌握图像编辑的关键在于:

  • 理解 Bitmap 内存模型:知道如何控制 Bitmap 大小(采样率、缩放),避免 OOM;
  • 掌握像素操作技巧:批量处理像素、使用 RenderScript 加速,避免卡顿;
  • 平衡效果与性能:根据需求选择合适的实现方式(如简单圆角用RoundedBitmapDrawable,复杂滤镜用 RenderScript)。

通过本文的方法,你可以实现从基础到进阶的各类图像编辑功能,同时避开内存和性能陷阱。图像编辑的核心不是 “实现功能”,而是 “高质量、高效率地实现功能”—— 这需要在实践中不断优化,根据具体场景调整方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Monkey-旭

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值