22、水印与文字叠加:Bitmap叠加、文字渲染、位置控制、透明度混合

水印叠加,说白了就是在视频画面上再盖一层东西。这东西可以是图片,也可以是文字。我在项目里遇到过不少次——客户要求把Logo打在视频右上角,或者把时间戳显示在左下角。嗯,今天我们就来聊聊这个。

Bitmap叠加:把图片贴到画面上

先说说最简单的场景:把一张PNG图片贴到视频帧上。Android里我们通常用Canvas来做这件事。

// 加载水印图片
Bitmap watermark = BitmapFactory.decodeResource(getResources(), R.drawable.logo);

// 创建与视频帧等大的Bitmap作为画布
Bitmap frameBitmap = Bitmap.createBitmap(videoWidth, videoHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(frameBitmap);

// 先画视频帧
canvas.drawBitmap(videoFrame, 0, 0, null);

// 再画水印
canvas.drawBitmap(watermark, posX, posY, null);

// 搞定,frameBitmap就是叠加后的结果

这里有个坑——Bitmap的格式。我建议用ARGB_8888,别用RGB_565。为什么?因为RGB_565不支持透明通道,你的水印边缘会带白边,特别难看。我曾经在这个问题上折腾了一下午,最后发现是格式选错了。

注意:BitmapFactory.decodeResource()出来的图片,如果原图是ARGB,它默认会保持。但如果你从网络加载或者从文件读取,一定要检查Config。不放心的话,手动转一下:
watermark = watermark.copy(Bitmap.Config.ARGB_8888, true);

文字渲染:不只是drawText那么简单

文字叠加比图片稍微复杂一点。你想想看,文字有字体、大小、颜色、对齐方式……这些都得考虑。

Paint textPaint = new Paint();
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(48);
textPaint.setAntiAlias(true);
textPaint.setTypeface(Typeface.DEFAULT_BOLD);

// 测量文字宽度
float textWidth = textPaint.measureText("Hello World");

// 绘制文字
canvas.drawText("Hello World", posX, posY, textPaint);

嗯,这里要注意一个细节:drawText的坐标是基线位置,不是文字左上角。如果你想把文字放在某个矩形区域内,得先算一下基线偏移。

小技巧:用Paint.FontMetrics来获取基线偏移量。
Paint.FontMetrics fm = textPaint.getFontMetrics();
float baselineY = rectCenterY - (fm.ascent + fm.descent) / 2;
这样文字就能垂直居中了。

位置控制:九宫格定位法

在实际项目中,水印位置通常是可配置的。我个人习惯用九宫格定位——把画面分成3x3的格子,用户选择左上、右上、居中、左下、右下等位置。

public enum WatermarkPosition {
    TOP_LEFT, TOP_CENTER, TOP_RIGHT,
    CENTER_LEFT, CENTER, CENTER_RIGHT,
    BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT
}

public Point calculatePosition(int frameW, int frameH, 
                               int watermarkW, int watermarkH,
                               WatermarkPosition pos, int margin) {
    int x, y;
    switch (pos) {
        case TOP_LEFT:
            x = margin;
            y = margin;
            break;
        case TOP_RIGHT:
            x = frameW - watermarkW - margin;
            y = margin;
            break;
        case BOTTOM_LEFT:
            x = margin;
            y = frameH - watermarkH - margin;
            break;
        case BOTTOM_RIGHT:
            x = frameW - watermarkW - margin;
            y = frameH - watermarkH - margin;
            break;
        case CENTER:
            x = (frameW - watermarkW) / 2;
            y = (frameH - watermarkH) / 2;
            break;
        // ... 其他位置类似
    }
    return new Point(x, y);
}

margin参数很重要。我建议留出10-20像素的边距,别让水印紧贴着画面边缘,那样看起来不够专业。

透明度混合:让水印不那么突兀

直接贴上去的水印太生硬了。你想想看,视频画面本身是动态的,水印如果完全不透明,会遮挡太多内容。所以我们需要透明度混合。

Android里控制透明度有两种方式:

  1. Paint的Alpha:直接设置画笔的透明度
  2. Bitmap的Alpha:修改Bitmap每个像素的Alpha通道
// 方式一:Paint Alpha
Paint paint = new Paint();
paint.setAlpha(128); // 半透明,范围0-255
canvas.drawBitmap(watermark, posX, posY, paint);

// 方式二:Bitmap Alpha(更精细)
Bitmap alphaWatermark = Bitmap.createBitmap(watermark.getWidth(), 
    watermark.getHeight(), Bitmap.Config.ARGB_8888);
Canvas alphaCanvas = new Canvas(alphaWatermark);
Paint alphaPaint = new Paint();
alphaPaint.setAlpha(128);
alphaCanvas.drawBitmap(watermark, 0, 0, alphaPaint);
// 然后用alphaWatermark去叠加

我个人更推荐方式一,因为不需要额外创建Bitmap,性能更好。但如果你需要对水印的不同区域设置不同透明度,那就得用方式二了。

核心要点:透明度混合的本质是Alpha Blending。公式很简单:
result = foreground * alpha + background * (1 - alpha)
在Android里,Canvas默认就是做这个计算。你只要设置好Paint的Alpha,剩下的交给系统。

性能优化:别在每一帧都创建对象

做视频处理最怕什么?GC抖动。如果你在每一帧都new Paint、new Canvas、new Bitmap,那视频帧率会惨不忍睹。

我的做法是:在初始化阶段就把所有对象创建好,循环复用

// 初始化
private Paint mWatermarkPaint;
private Canvas mCanvas;
private Bitmap mFrameBitmap;

public void init(int width, int height) {
    mWatermarkPaint = new Paint();
    mWatermarkPaint.setAntiAlias(true);
    mWatermarkPaint.setFilterBitmap(true);
    
    mFrameBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    mCanvas = new Canvas(mFrameBitmap);
}

// 每一帧只调用这个方法
public Bitmap processFrame(Bitmap videoFrame, Bitmap watermark, int alpha) {
    mWatermarkPaint.setAlpha(alpha);
    mCanvas.drawBitmap(videoFrame, 0, 0, null);
    mCanvas.drawBitmap(watermark, posX, posY, mWatermarkPaint);
    return mFrameBitmap;
}

你看,这样每一帧只做绘制操作,没有对象创建,性能就好多了。

SVG:水印叠加流程

水印叠加处理流程 视频帧 (Bitmap) 水印 (Bitmap/文字) 位置计算 九宫格定位 + 边距 透明度设置 Paint.setAlpha(0-255) Canvas绘制 先画视频帧,再画水印 叠加后的视频帧

避坑指南

最后分享几个我踩过的坑:

  • Bitmap复用问题:千万别在多个线程同时操作同一个Bitmap。我遇到过crash,查了半天发现是渲染线程和编码线程在抢同一个Bitmap。
  • 文字锯齿:设置setAntiAlias(true)还不够,记得setDither(true)和setFilterBitmap(true),特别是水印有缩放的时候。
  • 性能瓶颈:如果视频分辨率是1080p甚至4K,每一帧都做Bitmap叠加会很慢。我建议用OpenGL或者RenderScript来做硬件加速,效率能提升好几倍。
  • 文字换行:drawText不支持自动换行。如果需要多行文字,得用StaticLayout或者自己手动拆分行。
我的习惯:在项目初期,先用纯软件方案把功能跑通,验证效果。等所有逻辑都稳定了,再考虑用OpenGL做性能优化。这样调试起来方便,不容易出bug。

好了,水印叠加这块就聊这么多。说白了就是三步:准备素材、计算位置、绘制合成。但每一步都有细节要注意。你动手试试看,遇到问题欢迎交流。


公众号:蓝海资料掘金营,微信deep3321