一、视频缩放与裁剪:从Matrix到裁剪模式

视频渲染中,缩放和裁剪是绕不开的话题。说白了,就是怎么把视频画面塞进你的SurfaceView里。我刚开始做视频播放器时,总觉得这很简单——不就是调个大小吗?结果第一次遇到不同分辨率的视频源,画面要么被拉伸变形,要么显示不全,用户反馈直接炸了。

嗯,今天我们就来彻底搞懂这件事。

1.1 Matrix变换:一切缩放裁剪的基础

Android里的Matrix,本质上是一个3x3的变换矩阵。它控制着坐标的平移、缩放、旋转和错切。对于视频渲染,我们主要用它来做两件事:缩放平移

我个人习惯把Matrix理解为「一个可以随时调整的投影仪镜头」。你旋转旋钮,画面就放大缩小;你移动镜头,画面就上下左右偏移。

核心概念:Matrix操作的是「纹理坐标」到「屏幕坐标」的映射关系。你改变Matrix,本质上是在改变视频帧的绘制区域。

来看一段实际代码。假设我们要把视频画面放大1.5倍,并居中显示:

Matrix matrix = new Matrix();
// 先缩放,后平移
matrix.setScale(1.5f, 1.5f, pivotX, pivotY);
// 或者用setScaleX/setScaleY单独控制
matrix.setScaleX(1.5f);
matrix.setScaleY(1.5f);
// 应用给SurfaceView的Canvas
canvas.drawBitmap(videoFrame, matrix, null);

这里有个坑,我踩过好几次——setScale和postScale的区别。setScale是直接设置,会覆盖之前的变换;postScale是追加变换,会保留之前的操作。如果你先平移再缩放,用setScale会把平移也清掉。所以,我建议统一用postXxx系列方法,或者每次重新构建Matrix。

1.2 setScaleX/setScaleY:精细控制每一轴

这两个方法,说白了就是分别控制X轴和Y轴的缩放比例。为什么需要分开控制?因为视频源的分辨率和屏幕分辨率往往不一致。

举个例子:视频是1920x1080(16:9),你的SurfaceView是1080x1920(9:16)。这时候如果统一缩放,画面会被压扁。你需要让X轴和Y轴各自独立缩放,才能保持比例。

Matrix matrix = new Matrix();
float scaleX = (float) surfaceWidth / videoWidth;
float scaleY = (float) surfaceHeight / videoHeight;
matrix.setScaleX(scaleX);
matrix.setScaleY(scaleY);

我在项目中遇到过一种情况:某些低端设备上,setScaleX/setScaleY的精度不够,导致画面边缘出现1像素的锯齿。解决方案是改用setScale(float, float, float, float)方法,指定缩放中心点,精度会更高。

小技巧:如果你只需要保持宽高比,不要手动计算scaleX和scaleY。直接用Matrix的setRectToRect方法,它会自动帮你算好。

1.3 视频裁剪模式:CENTER_CROP / FIT_CENTER / FIT_XY

这三种模式,是Android系统预定义的裁剪策略。你可以在ImageView里看到它们,但用在SurfaceView上,原理完全一样。

模式 行为 适用场景
CENTER_CROP 保持比例,缩放至填满容器,裁剪超出部分 视频背景、全屏播放(允许裁边)
FIT_CENTER 保持比例,缩放至完全显示,留黑边 视频预览、缩略图(必须看到完整画面)
FIT_XY 不保持比例,拉伸至填满容器 游戏画面、特殊效果(允许变形)

这三种模式的实现,本质上就是Matrix的不同计算方式。我画了一张图,帮你理解它们的区别:

视频裁剪模式对比 SurfaceView 视频画面 16:9 CENTER_CROP 填满容器 上下裁切 FIT_CENTER 完整显示 左右留黑边 注意:FIT_XY未画出,它会直接拉伸画面填满容器,比例会失真

1.4 自定义裁剪算法:当系统模式不够用时

系统提供的三种模式,覆盖了大部分场景。但总有一些特殊情况,比如:

  • 用户想要「顶部对齐裁剪」(类似Instagram的封面裁剪)
  • 视频画面需要旋转后再裁剪
  • 多个视频拼接时,每个视频的裁剪区域不同

这时候,你就得自己写裁剪算法了。核心思路其实很简单:先计算缩放比例,再计算偏移量

我曾经做过一个视频编辑功能,用户需要自由拖动裁剪框。当时我写了一个通用的裁剪算法:

public Matrix calculateCropMatrix(
    int videoWidth, int videoHeight,
    int surfaceWidth, int surfaceHeight,
    float cropLeft, float cropTop, 
    float cropRight, float cropBottom) {
    
    Matrix matrix = new Matrix();
    
    // 1. 计算视频的宽高比
    float videoAspect = (float) videoWidth / videoHeight;
    float surfaceAspect = (float) surfaceWidth / surfaceHeight;
    
    // 2. 计算基础缩放(保持比例)
    float scale;
    float offsetX = 0, offsetY = 0;
    
    if (videoAspect > surfaceAspect) {
        // 视频更宽:按高度缩放
        scale = (float) surfaceHeight / videoHeight;
        offsetX = (surfaceWidth - videoWidth * scale) / 2f;
    } else {
        // 视频更高:按宽度缩放
        scale = (float) surfaceWidth / videoWidth;
        offsetY = (surfaceHeight - videoHeight * scale) / 2f;
    }
    
    // 3. 应用裁剪区域(归一化坐标 0~1)
    float cropWidth = cropRight - cropLeft;
    float cropHeight = cropBottom - cropTop;
    float cropScaleX = 1f / cropWidth;
    float cropScaleY = 1f / cropHeight;
    
    // 4. 组合变换:先平移裁剪区域到原点,再缩放
    matrix.preTranslate(-cropLeft * videoWidth, -cropTop * videoHeight);
    matrix.preScale(cropScaleX, cropScaleY);
    matrix.postScale(scale, scale);
    matrix.postTranslate(offsetX, offsetY);
    
    return matrix;
}
注意:自定义裁剪时,一定要处理好「边缘像素」问题。如果裁剪区域刚好在视频边缘,可能会出现半透明或黑边。我建议在裁剪区域外预留2-3个像素的缓冲。

1.5 性能优化:别让Matrix成为瓶颈

Matrix操作本身很快,但如果你每帧都重新创建Matrix对象,GC就会频繁触发。我见过一个项目,每帧都new Matrix(),结果在低端机上卡成PPT。

我的建议是:

  • 复用Matrix对象:在初始化时创建,每帧只调用set/post方法
  • 避免在渲染线程中做复杂计算:裁剪参数变化时,在UI线程算好Matrix,渲染线程只负责应用
  • 使用硬件加速:SurfaceView默认支持硬件加速,Matrix变换会由GPU完成,CPU只负责计算参数
避坑指南:我曾经在某个项目里,把Matrix的逆矩阵计算放在渲染循环中。结果发现每帧计算逆矩阵消耗了2ms。后来改成缓存逆矩阵,只在Matrix变化时重新计算,性能直接翻倍。

1.6 实战:实现一个可切换裁剪模式的视频播放器

最后,我们把这些知识串起来。假设你要做一个视频播放器,支持用户切换裁剪模式:

public enum CropMode {
    CENTER_CROP, FIT_CENTER, FIT_XY, CUSTOM
}

public Matrix getMatrixForMode(CropMode mode, 
                               int vw, int vh, 
                               int sw, int sh) {
    Matrix matrix = new Matrix();
    float sx = (float) sw / vw;
    float sy = (float) sh / vh;
    
    switch (mode) {
        case FIT_XY:
            matrix.setScale(sx, sy);
            break;
        case FIT_CENTER:
            float scale = Math.min(sx, sy);
            float dx = (sw - vw * scale) / 2f;
            float dy = (sh - vh * scale) / 2f;
            matrix.setScale(scale, scale);
            matrix.postTranslate(dx, dy);
            break;
        case CENTER_CROP:
            scale = Math.max(sx, sy);
            dx = (sw - vw * scale) / 2f;
            dy = (sh - vh * scale) / 2f;
            matrix.setScale(scale, scale);
            matrix.postTranslate(dx, dy);
            break;
        case CUSTOM:
            // 调用自定义算法
            matrix = calculateCropMatrix(vw, vh, sw, sh, 
                                         0.1f, 0.1f, 0.9f, 0.9f);
            break;
    }
    return matrix;
}

嗯,到这里,视频缩放与裁剪的核心内容就讲完了。你可能会问:为什么不用SurfaceView自带的setScaleX/setScaleY?因为那些方法操作的是View本身,而不是视频内容。我们要操作的是「视频帧的绘制区域」,所以必须用Matrix来控制Canvas的变换。

记住一句话:Matrix是视频渲染的「遥控器」。你按哪个键,画面就怎么动。掌握了Matrix,你就掌握了视频渲染的主动权。


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