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不支持透明通道,你的水印边缘会带白边,特别难看。我曾经在这个问题上折腾了一下午,最后发现是格式选错了。
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 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里控制透明度有两种方式:
- Paint的Alpha:直接设置画笔的透明度
- 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,性能更好。但如果你需要对水印的不同区域设置不同透明度,那就得用方式二了。
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。我遇到过crash,查了半天发现是渲染线程和编码线程在抢同一个Bitmap。
- 文字锯齿:设置setAntiAlias(true)还不够,记得setDither(true)和setFilterBitmap(true),特别是水印有缩放的时候。
- 性能瓶颈:如果视频分辨率是1080p甚至4K,每一帧都做Bitmap叠加会很慢。我建议用OpenGL或者RenderScript来做硬件加速,效率能提升好几倍。
- 文字换行:drawText不支持自动换行。如果需要多行文字,得用StaticLayout或者自己手动拆分行。
好了,水印叠加这块就聊这么多。说白了就是三步:准备素材、计算位置、绘制合成。但每一步都有细节要注意。你动手试试看,遇到问题欢迎交流。
公众号:蓝海资料掘金营,微信deep3321