镜像水平翻转一个 Bitmap,代码如下:

@NonNull
private ByteArrayOutputStream flipImageHorizontally() {
    ByteBuffer buffer = image.getPlanes()[0].getBuffer();
    byte[] bytes = new byte[buffer.remaining()];
    buffer.get(bytes);
    final Bitmap origin = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
    Matrix matrix = new Matrix();
    matrix.preScale(-1.0f, 1.0f);
    Bitmap hFlip = Bitmap.createBitmap(origin, 0, 0, origin.getWidth(), origin.getHeight(), matrix, true);
    return hFlip;
}

看下 Bitmap.createBitmap 里面做了什么,传递一个 matrix.preScale(-1.0f, 1.0f) 就能实现镜像 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) {
    int neww = width;
    int newh = height;
    Bitmap bitmap;
    Paint paint;
    Rect srcR = new Rect(x, y, x + width, y + height);
    RectF dstR = new RectF(0, 0, width, height);
    RectF deviceR = new RectF();
    if (m == null || m.isIdentity()) {
        bitmap = createBitmap(null, neww, newh, newConfig, source.hasAlpha(), cs);
        paint = null;   // not needed
    } else {
        final boolean transformed = !m.rectStaysRect();
        m.mapRect(deviceR, dstR);
        neww = Math.round(deviceR.width());
        newh = Math.round(deviceR.height());
        bitmap = createBitmap(null, neww, newh, transformedConfig,
                transformed || source.hasAlpha(), cs);
        paint = new Paint();
        paint.setFilterBitmap(filter);
        if (transformed) {
            paint.setAntiAlias(true);
        }
    }
    // The new bitmap was created from a known bitmap source so assume that
    // they use the same density
    bitmap.mDensity = source.mDensity;
    bitmap.setHasAlpha(source.hasAlpha());
    bitmap.setPremultiplied(source.mRequestPremultiplied);
    Canvas canvas = new Canvas(bitmap);
    canvas.translate(-deviceR.left, -deviceR.top);
    canvas.concat(m);
    canvas.drawBitmap(source, srcR, dstR, paint);
    canvas.setBitmap(null);
    if (isHardware) {
        return bitmap.copy(Config.HARDWARE, false);
    }
    return bitmap;
}
canvas.translate(-deviceR.left, -deviceR.top);
canvas.concat(m);

# 矩阵前乘

translate: preconcat the current matrix with the specified translation.
concat: preconcat the current matrix with the specified matrix. If the specified matrix is null, this method does nothing.
preconcat (前乘) 是怎么运算的呢?拿 Matrix.preTranslate 为范例。 M.preTranslate 的结果 M' = M * T(dx, dy)

/**
 * Preconcats the matrix with the specified translation. M' = M * T(dx, dy)
 */
public boolean preTranslate(float dx, float dy) {
    nPreTranslate(native_instance, dx, dy);
    return true;
}

canvas 初始化后的 matrix 是单位矩阵 I, 那么最后的矩阵 M` = I * translate * m, Canvas 是从右向左应用矩阵。

为什么是这样的矩阵链,我们该如何理解?

# 坐标系

Canvas 底层的实现是 Skia, 其有 device 和 local 坐标系。 device 坐标系由你要渲染到的 surface 定义,他们的范围从 surface 的左上角 (0, 0) 到右下角的 (w, h)。 local 坐标系是所有 geometry & shader 应用到 SkCanvas 时的参考坐标系。开始时,两个坐标系原点重合。矩阵变换操作的是 local 坐标系, canvas 定义在 local 坐标系上。

Device coordinates are defined by the surface (or other device) that you’re rendering to. They range from (0, 0) in the upper-left corner of the surface, to (w, h) in the bottom-right corner - they are effectively measured in pixels.

The local coordinate space is how all geometry and shaders are supplied to the SkCanvas. By default, the local and device coordinate systems are the same.

Matrix matrix = new Matrix();
matrix.preScale(-1.0f, 1.0f);
RectF dstR = new RectF(0, 0, width, height);
Canvas canvas = new Canvas(bitmap);
canvas.translate(-deviceR.left, -deviceR.top);
canvas.concat(m);
canvas.drawBitmap(source, srcR, dstR, paint);
  1. drawBitmap 时,对 canvas 应用 matrix ,canvas 上的 dstR 在局部坐标系定义,也跟随 matrix 变换。dstR 应用镜像矩阵变换后在 device 坐标系变成 (-width, 0 - 0, height),同时 canvas 也一起水平翻转,此时 dstR 里坐标(0, 0) 的像素映射绘制到 device 坐标系里的 (0, 0),dstR 里坐标(width, 0)的像素映射绘制到 device 坐标系里的 (-width, 0)。

  2. canvas 应用 matrix 后, local 坐标系可能发生位移,偏离 device 坐标系原点,dstR 在 local 坐标系不变,但在 device 坐标系同 local 坐标系一样,可能发生位移和缩放。 canvas 矩阵需要前乘这个逆位移,将 dstR 按矩阵 m 变换后的 deviceR 的左上角对齐 device 坐标系原点。 然后将 bitmap 的 srcR 区域像素,采样填充到 canvas 坐标系下的 dstR。

dstR 应用矩阵 m 后,跑到 device 坐标系哪里去了呢?答案就在 m.mapRect 后的 deviceR 里,dstR 左上角矩阵变换后,映射到 deviceR 的左上角。

m.mapRect(deviceR, dstR);

对于 local 坐标系的某一点 A (left, top),如果要偏移到 device 坐标系的原点,则只需要 canvas.translate(-left, -top)
canvas 有自己的坐标系,其原点位于 View 的左上角,X 轴朝右, Y 轴朝下。在 drawBitmap 前的 translate,scale,rotate, concat 等矩阵操作其实都是在三维空间里对 Canvas 即 local 坐标系做变换。


偏移前,Local 坐标系和 Device 坐标系重合在原点


偏移(-left, -top)后,Local 坐标系和 Device 坐标系分离,A 点位于 Device 坐标系原点

Edited on Views times

Give me a cup of [coffee]~( ̄▽ ̄)~*

文理兼修电脑首席 WeChat Pay

WeChat Pay

文理兼修电脑首席 Alipay

Alipay