Android多种方式实现相机圆形预览

9次阅读

共计 19805 个字符,预计需要花费 50 分钟才能阅读完成。

效果图如下:

一、为预览控件设置圆角

为控件设置 ViewOutlineProvider

public RoundTextureView(Context context, AttributeSet attrs) {super(context, attrs);
    setOutlineProvider(new ViewOutlineProvider() {
        @Override
        public void getOutline(View view, Outline outline) {Rect rect = new Rect(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
            outline.setRoundRect(rect, radius);
        }
    });
    setClipToOutline(true);
}

在需要时修改圆角值并更新

public void setRadius(int radius) {this.radius = radius;}

public void turnRound() {invalidateOutline();
}

即可根据设置的圆角值更新控件显示的圆角大小。当控件为正方形,且圆角值为边长的一半,显示的就是圆形。

二、实现正方形预览

1. 设备支持 1:1 预览尺寸
首先介绍一种简单但是局限性较大的实现方式:将相机预览尺寸和预览控件的大小都调整为 1:1。
一般 Android 设备都支持多种预览尺寸,以 Samsung Tab S3 为例

在使用 Camera API 时,其支持的预览尺寸如下:

2019-08-02 13:16:08.669 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 1920x1080
2019-08-02 13:16:08.669 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 1280x720
2019-08-02 13:16:08.669 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 1440x1080
2019-08-02 13:16:08.669 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 1088x1088
2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 1056x864
2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 960x720
2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 720x480
2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 640x480
2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 352x288
2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 320x240
2019-08-02 13:16:08.670 16407-16407/com.wsy.glcamerademo I/CameraHelper: supportedPreviewSize: 176x144

其中 1:1 的预览尺寸为:1088×1088。

在使用 Camera2 API 时,其支持的预览尺寸(其实也包含了 PictureSize)如下:

2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 4128x3096
2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 4128x2322
2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 3264x2448
2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 3264x1836
2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 3024x3024
2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2976x2976
2019-08-02 13:19:24.980 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2880x2160
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2592x1944
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2560x1920
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2560x1440
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2560x1080
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2160x2160
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2048x1536
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 2048x1152
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 1936x1936
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 1920x1080
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 1440x1080
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 1280x960
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 1280x720
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 960x720
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 720x480
2019-08-02 13:19:24.981 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 640x480
2019-08-02 13:19:24.982 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 320x240
2019-08-02 13:19:24.982 16768-16768/com.wsy.glcamerademo I/Camera2Helper: getBestSupportedSize: 176x144

其中 1:1 的预览尺寸为:3024×3024、2976×2976、2160×2160、1936×1936。
只要我们选择 1:1 的预览尺寸,再将预览控件设置为正方形,即可实现正方形预览;
再通过设置预览控件的圆角为边长的一半,即可实现圆形预览。2. 设备不支持 1:1 预览尺寸的情况

选择 1:1 预览尺寸的缺陷分析

分辨率局限性
上述说到,我们可以选择 1:1 的预览尺寸进行预览,但是局限性较高,
可选择范围都很小。如果相机不支持 1:1 的预览尺寸,这个方案就不可行了。

资源消耗
以 Samsung tab S3 为例,该设备使用 Camera2 API 时,支持的正方形预览尺寸都很大,在进行图像处理等操作时将占用较多系统资源。

处理不支持 1:1 预览尺寸的情况

添加一个 1:1 尺寸的 ViewGroup
将 TextureView 放入 ViewGroup
设置 TextureView 的 margin 值以达到显示中心正方形区域的效果

示意图

示例代码

// 将预览控件和预览尺寸比例保持一致,避免拉伸
{FrameLayout.LayoutParams textureViewLayoutParams = (FrameLayout.LayoutParams) textureView.getLayoutParams();
    int newHeight = 0;
    int newWidth = textureViewLayoutParams.width;
    // 横屏
    if (displayOrientation % 180 == 0) {newHeight = textureViewLayoutParams.width * previewSize.height / previewSize.width;}
    // 竖屏
    else {newHeight = textureViewLayoutParams.width * previewSize.width / previewSize.height;}
    //// 当不是正方形预览的情况下,添加一层 ViewGroup 限制 View 的显示区域
    if (newHeight != textureViewLayoutParams.height) {insertFrameLayout = new RoundFrameLayout(CoverByParentCameraActivity.this);
       int sideLength = Math.min(newWidth, newHeight);
       FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(sideLength, sideLength);
       insertFrameLayout.setLayoutParams(layoutParams);
       FrameLayout parentView = (FrameLayout) textureView.getParent();
       parentView.removeView(textureView);
       parentView.addView(insertFrameLayout);

       insertFrameLayout.addView(textureView);
       FrameLayout.LayoutParams newTextureViewLayoutParams = new FrameLayout.LayoutParams(newWidth, newHeight);
       // 横屏
       if (displayOrientation % 180 == 0) {newTextureViewLayoutParams.leftMargin = ((newHeight - newWidth) / 2);
       }
       // 竖屏
       else {newTextureViewLayoutParams.topMargin = -(newHeight - newWidth) / 2;
       }
       textureView.setLayoutParams(newTextureViewLayoutParams);
    }
}

三、使用 GLSurfaceView 进行自定义程度更高的预览

使用上面的方法操作已经可完成正方形和圆形预览,但是仅适用于原生相机,当我们的数据源并非是原生相机的情况时如何进行圆形预览?接下来介绍使用 GLSurfaceView 显示 NV21 的方案,完全是自己实现预览数据的绘制。
1. GLSurfaceView 使用流程

OpenGL 渲染 YUV 数据流程

其中的重点是渲染器(Renderer)的编写,Renderer 的介绍如下:
/**

 * A generic renderer interface.
 * <p>
 * The renderer is responsible for making OpenGL calls to render a frame.
 * <p>
 * GLSurfaceView clients typically create their own classes that implement
 * this interface, and then call {@link GLSurfaceView#setRenderer} to
 * register the renderer with the GLSurfaceView.
 * <p>
 *
 * <div class="special reference">
 * <h3>Developer Guides</h3>
 * <p>For more information about how to use OpenGL, read the
 * <a href="{@docRoot}guide/topics/graphics/opengl.html">OpenGL</a> developer guide.</p>
 * </div>
 *
 * <h3>Threading</h3>
 * The renderer will be called on a separate thread, so that rendering
 * performance is decoupled from the UI thread. Clients typically need to
 * communicate with the renderer from the UI thread, because that's where
 * input events are received. Clients can communicate using any of the
 * standard Java techniques for cross-thread communication, or they can
 * use the {@link GLSurfaceView#queueEvent(Runnable)} convenience method.
 * <p>
 * <h3>EGL Context Lost</h3>
 * There are situations where the EGL rendering context will be lost. This
 * typically happens when device wakes up after going to sleep. When
 * the EGL context is lost, all OpenGL resources (such as textures) that are
 * associated with that context will be automatically deleted. In order to
 * keep rendering correctly, a renderer must recreate any lost resources
 * that it still needs. The {@link #onSurfaceCreated(GL10, EGLConfig)} method
 * is a convenient place to do this.
 *
 *
 * @see #setRenderer(Renderer)
 */
public interface Renderer {
    /**
     * Called when the surface is created or recreated.
     * <p>
     * Called when the rendering thread
     * starts and whenever the EGL context is lost. The EGL context will typically
     * be lost when the Android device awakes after going to sleep.
     * <p>
     * Since this method is called at the beginning of rendering, as well as
     * every time the EGL context is lost, this method is a convenient place to put
     * code to create resources that need to be created when the rendering
     * starts, and that need to be recreated when the EGL context is lost.
     * Textures are an example of a resource that you might want to create
     * here.
     * <p>
     * Note that when the EGL context is lost, all OpenGL resources associated
     * with that context will be automatically deleted. You do not need to call
     * the corresponding "glDelete" methods such as glDeleteTextures to
     * manually delete these lost resources.
     * <p>
     * @param gl the GL interface. Use <code>instanceof</code> to
     * test if the interface supports GL11 or higher interfaces.
     * @param config the EGLConfig of the created surface. Can be used
     * to create matching pbuffers.
     */
    void onSurfaceCreated(GL10 gl, EGLConfig config);

    /**
     * Called when the surface changed size.
     * <p>
     * Called after the surface is created and whenever
     * the OpenGL ES surface size changes.
     * <p>
     * Typically you will set your viewport here. If your camera
     * is fixed then you could also set your projection matrix here:
     * <pre class="prettyprint">
     * void onSurfaceChanged(GL10 gl, int width, int height) {*     gl.glViewport(0, 0, width, height);
     *     // for a fixed camera, set the projection too
     *     float ratio = (float) width / height;
     *     gl.glMatrixMode(GL10.GL_PROJECTION);
     *     gl.glLoadIdentity();
     *     gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
     * }
     * </pre>
     * @param gl the GL interface. Use <code>instanceof</code> to
     * test if the interface supports GL11 or higher interfaces.
     * @param width
     * @param height
     */
    void onSurfaceChanged(GL10 gl, int width, int height);

    /**
     * Called to draw the current frame.
     * <p>
     * This method is responsible for drawing the current frame.
     * <p>
     * The implementation of this method typically looks like this:
     * <pre class="prettyprint">
     * void onDrawFrame(GL10 gl) {*     gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
     *     //... other gl calls to render the scene ...
     * }
     * </pre>
     * @param gl the GL interface. Use <code>instanceof</code> to
     * test if the interface supports GL11 or higher interfaces.
     */
    void onDrawFrame(GL10 gl);
}



void onSurfaceCreated(GL10 gl, EGLConfig config)
在 Surface 创建或重建的情况下回调

void onSurfaceChanged(GL10 gl, int width, int height)
在 Surface 的大小发生变化的情况下回调

void onDrawFrame(GL10 gl)
在这里实现绘制操作。当我们设置的 renderMode 为 RENDERMODE_CONTINUOUSLY 时,该函数将不断地执行;
当我们设置的 renderMode 为 RENDERMODE_WHEN_DIRTY 时,将只在创建完成和调用 requestRender 后才执行。一般我们选择 RENDERMODE_WHEN_DIRTY 渲染模式,避免过度绘制。

一般情况下,我们会自己实现一个 Renderer,然后为 GLSurfaceView 设置 Renderer,可以说,Renderer 的编写是整个流程的核心步骤。以下是在 void onSurfaceCreated(GL10 gl, EGLConfig config)进行的初始化操作和在 void onDrawFrame(GL10 gl)进行的绘制操作的流程图:

渲染 YUV 数据的 Renderer

2. 具体实现

坐标系介绍

Android View 坐标系

OpenGL 世界坐标系

如图所示,和 Android 的 View 坐标系不同,OpenGL 的坐标系是笛卡尔坐标系。
Android View 的坐标系以左上角为原点,向右 x 递增,向下 y 递增;
而 OpenGL 坐标系以中心为原点,向右 x 递增,向上 y 递增。

着色器编写

/**
 * 顶点着色器
 */
private static String VERTEX_SHADER =
        "attribute vec4 attr_position;\n" +
                "attribute vec2 attr_tc;\n" +
                "varying vec2 tc;\n" +
                "void main() {\n" +
                "gl_Position = attr_position;\n" +
                "tc = attr_tc;\n" +
                "}";

/**
 * 片段着色器
 */
private static String FRAG_SHADER =
        "varying vec2 tc;\n" +
                "uniform sampler2D ySampler;\n" +
                "uniform sampler2D uSampler;\n" +
                "uniform sampler2D vSampler;\n" +
                "const mat3 convertMat = mat3(1.0, 1.0, 1.0, -0.001, -0.3441, 1.772, 1.402, -0.7141, -0.58060);\n" +
                "void main()\n" +
                "{\n" +
                "vec3 yuv;\n" +
                "yuv.x = texture2D(ySampler, tc).r;\n" +
                "yuv.y = texture2D(uSampler, tc).r - 0.5;\n" +
                "yuv.z = texture2D(vSampler, tc).r - 0.5;\n" +
                "gl_FragColor = vec4(convertMat * yuv, 1.0);\n" +
                "}";


内建变量解释

gl_Position

VERTEX_SHADER代码里的 gl_Position 代表绘制的空间坐标。由于我们是二维绘制,所以直接传入 OpenGL 二维坐标系的左下(-1,-1)、右下(1,-1)、左上(-1,1)、右上(1,1),也就是{-1,-1,1,-1,-1,1,1,1}

gl_FragColor

FRAG_SHADER代码里的 gl_FragColor 代表单个片元的颜色

其他变量解释

ySampler、uSampler、vSampler

分别代表 Y、U、V 纹理采样器

convertMat

根据以下公式:

R = Y + 1.402 (V - 128)
G = Y - 0.34414 (U - 128) - 0.71414 (V - 128)
B = Y + 1.772 (U - 128)

我们可得到一个 YUV 转 RGB 的矩阵

1.0,    1.0,    1.0, 
0,     -0.344,  1.77, 
1.403, -0.714,  0 

部分类型、函数的解释

vec3、vec4

分别代表三维向量、四维向量。

vec4 texture2D(sampler2D sampler, vec2 coord)

以指定的矩阵将采样器的图像纹理转换为颜色值;如:
texture2D(ySampler, tc).r获取到的是 Y 数据,
texture2D(uSampler, tc).r获取到的是 U 数据,
texture2D(vSampler, tc).r获取到的是 V 数据。

在 Java 代码中进行初始化
根据图像宽高创建 Y、U、V 对应的 ByteBuffer 纹理数据;
根据是否镜像显示、旋转角度选择对应的转换矩阵;

public void init(boolean isMirror, int rotateDegree, int frameWidth, int frameHeight) {
if (this.frameWidth == frameWidth
        && this.frameHeight == frameHeight
        && this.rotateDegree == rotateDegree
        && this.isMirror == isMirror) {return;}
dataInput = false;
this.frameWidth = frameWidth;
this.frameHeight = frameHeight;
this.rotateDegree = rotateDegree;
this.isMirror = isMirror;
yArray = new byte[this.frameWidth * this.frameHeight];
uArray = new byte[this.frameWidth * this.frameHeight / 4];
vArray = new byte[this.frameWidth * this.frameHeight / 4];

int yFrameSize = this.frameHeight * this.frameWidth;
int uvFrameSize = yFrameSize >> 2;
yBuf = ByteBuffer.allocateDirect(yFrameSize);
yBuf.order(ByteOrder.nativeOrder()).position(0);

uBuf = ByteBuffer.allocateDirect(uvFrameSize);
uBuf.order(ByteOrder.nativeOrder()).position(0);

vBuf = ByteBuffer.allocateDirect(uvFrameSize);
vBuf.order(ByteOrder.nativeOrder()).position(0);
// 顶点坐标
squareVertices = ByteBuffer
        .allocateDirect(GLUtil.SQUARE_VERTICES.length * FLOAT_SIZE_BYTES)
        .order(ByteOrder.nativeOrder())
        .asFloatBuffer();
squareVertices.put(GLUtil.SQUARE_VERTICES).position(0);
// 纹理坐标
if (isMirror) {switch (rotateDegree) {
        case 0:
            coordVertice = GLUtil.MIRROR_COORD_VERTICES;
            break;
        case 90:
            coordVertice = GLUtil.ROTATE_90_MIRROR_COORD_VERTICES;
            break;
        case 180:
            coordVertice = GLUtil.ROTATE_180_MIRROR_COORD_VERTICES;
            break;
        case 270:
            coordVertice = GLUtil.ROTATE_270_MIRROR_COORD_VERTICES;
            break;
        default:
            break;
    }
} else {switch (rotateDegree) {
        case 0:
            coordVertice = GLUtil.COORD_VERTICES;
            break;
        case 90:
            coordVertice = GLUtil.ROTATE_90_COORD_VERTICES;
            break;
        case 180:
            coordVertice = GLUtil.ROTATE_180_COORD_VERTICES;
            break;
        case 270:
            coordVertice = GLUtil.ROTATE_270_COORD_VERTICES;
            break;
        default:
            break;
    }
}
coordVertices = ByteBuffer.allocateDirect(coordVertice.length * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer();
coordVertices.put(coordVertice).position(0);

}

在 Surface 创建完成时进行 Renderer 初始化

    private void initRenderer() {
    rendererReady = false;
    createGLProgram();

    // 启用纹理
    GLES20.glEnable(GLES20.GL_TEXTURE_2D);
    // 创建纹理
    createTexture(frameWidth, frameHeight, GLES20.GL_LUMINANCE, yTexture);
    createTexture(frameWidth / 2, frameHeight / 2, GLES20.GL_LUMINANCE, uTexture);
    createTexture(frameWidth / 2, frameHeight / 2, GLES20.GL_LUMINANCE, vTexture);

    rendererReady = true;
}  

其中 createGLProgram 用于创建 OpenGL Program 并关联着色器代码中的变量

  private void createGLProgram() {int programHandleMain = GLUtil.createShaderProgram();
  if (programHandleMain != -1) {
      // 使用着色器程序
      GLES20.glUseProgram(programHandleMain);
      // 获取顶点着色器变量
      int glPosition = GLES20.glGetAttribLocation(programHandleMain, "attr_position");
      int textureCoord = GLES20.glGetAttribLocation(programHandleMain, "attr_tc");

      // 获取片段着色器变量
      int ySampler = GLES20.glGetUniformLocation(programHandleMain, "ySampler");
      int uSampler = GLES20.glGetUniformLocation(programHandleMain, "uSampler");
      int vSampler = GLES20.glGetUniformLocation(programHandleMain, "vSampler");

      // 给变量赋值
      /**
       * GLES20.GL_TEXTURE0 和 ySampler 绑定
       * GLES20.GL_TEXTURE1 和 uSampler 绑定
       * GLES20.GL_TEXTURE2 和 vSampler 绑定
       *
       * 也就是说 glUniform1i 的第二个参数代表图层序号
       */
      GLES20.glUniform1i(ySampler, 0);
      GLES20.glUniform1i(uSampler, 1);
      GLES20.glUniform1i(vSampler, 2);

      GLES20.glEnableVertexAttribArray(glPosition);
      GLES20.glEnableVertexAttribArray(textureCoord);

      /**
       * 设置 Vertex Shader 数据
       */
      squareVertices.position(0);
      GLES20.glVertexAttribPointer(glPosition, GLUtil.COUNT_PER_SQUARE_VERTICE, GLES20.GL_FLOAT, false, 8, squareVertices);
      coordVertices.position(0);
      GLES20.glVertexAttribPointer(textureCoord, GLUtil.COUNT_PER_COORD_VERTICES, GLES20.GL_FLOAT, false, 8, coordVertices);
  }

}

其中 createTexture 用于根据宽高和格式创建纹理

  private void createTexture(int width, int height, int format, int[] textureId) {
      // 创建纹理
      GLES20.glGenTextures(1, textureId, 0);
      // 绑定纹理
      GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId[0]);
      /**
       * {@link GLES20#GL_TEXTURE_WRAP_S}代表左右方向的纹理环绕模式
       * {@link GLES20#GL_TEXTURE_WRAP_T}代表上下方向的纹理环绕模式
       *
       *  {@link GLES20#GL_REPEAT}:重复
       *  {@link GLES20#GL_MIRRORED_REPEAT}:镜像重复
       *  {@link GLES20#GL_CLAMP_TO_EDGE}:忽略边框截取
       *
       * 例如我们使用{@link GLES20#GL_REPEAT}:*
       *             squareVertices           coordVertices
       *             -1.0f, -1.0f,            1.0f, 1.0f,
       *             1.0f, -1.0f,             1.0f, 0.0f,         ->          和 textureView 预览相同
       *             -1.0f, 1.0f,             0.0f, 1.0f,
       *             1.0f, 1.0f               0.0f, 0.0f
       *
       *             squareVertices           coordVertices
       *             -1.0f, -1.0f,            2.0f, 2.0f,
       *             1.0f, -1.0f,             2.0f, 0.0f,         ->          和 textureView 预览相比,分割成了 4 块相同的预览(左下,右下,左上,右上)*             -1.0f, 1.0f,             0.0f, 2.0f,
       *             1.0f, 1.0f               0.0f, 0.0f
       */
      GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
      GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);
      /**
       * {@link GLES20#GL_TEXTURE_MIN_FILTER}代表所显示的纹理比加载进来的纹理小时的情况
       * {@link GLES20#GL_TEXTURE_MAG_FILTER}代表所显示的纹理比加载进来的纹理大时的情况
       *
       *  {@link GLES20#GL_NEAREST}:使用纹理中坐标最接近的一个像素的颜色作为需要绘制的像素颜色
       *  {@link GLES20#GL_LINEAR}:使用纹理中坐标最接近的若干个颜色,通过加权平均算法得到需要绘制的像素颜色
       */
      GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
      GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
      GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, format, width, height, 0, format, GLES20.GL_UNSIGNED_BYTE, null);
  }

在 Java 代码中调用绘制

在数据源获取到时裁剪并传入帧数据

 @Override
  public void onPreview(final byte[] nv21, Camera camera) {
  // 裁剪指定的图像区域
  ImageUtil.cropNV21(nv21, this.squareNV21, previewSize.width, previewSize.height, cropRect);
  // 刷新 GLSurfaceView
  roundCameraGLSurfaceView.refreshFrameNV21(this.squareNV21);

}

NV21 数据裁剪代码

/**
* 裁剪 NV21 数据
*
* @param originNV21 原始的 NV21 数据
* @param cropNV21   裁剪结果 NV21 数据,需要预先分配内存
* @param width      原始数据的宽度
* @param height     原始数据的高度
* @param left       原始数据被裁剪的区域的左边界
* @param top        原始数据被裁剪的区域的上边界
* @param right      原始数据被裁剪的区域的右边界
* @param bottom     原始数据被裁剪的区域的下边界
*/
  public static void cropNV21(byte[] originNV21, byte[] cropNV21, int width, int height, int left, int top, int right, int bottom) {
  int halfWidth = width / 2;
  int cropImageWidth = right - left;
  int cropImageHeight = bottom - top;

  // 原数据 Y 左上
  int originalYLineStart = top * width;
  int targetYIndex = 0;

  // 原数据 UV 左上
  int originalUVLineStart = width * height + top * halfWidth;

  // 目标数据的 UV 起始值
  int targetUVIndex = cropImageWidth * cropImageHeight;

  for (int i = top; i < bottom; i++) {System.arraycopy(originNV21, originalYLineStart + left, cropNV21, targetYIndex, cropImageWidth);
      originalYLineStart += width;
      targetYIndex += cropImageWidth;
      if ((i & 1) == 0) {System.arraycopy(originNV21, originalUVLineStart + left, cropNV21, targetUVIndex, cropImageWidth);
          originalUVLineStart += width;
          targetUVIndex += cropImageWidth;
      }
  }

}

传给 GLSurafceView 并刷新帧数据

/**
* 传入 NV21 刷新帧
*
* @param data NV21 数据
*/
public void refreshFrameNV21(byte[] data) {if (rendererReady) {yBuf.clear();
      uBuf.clear();
      vBuf.clear();
      putNV21(data, frameWidth, frameHeight);
      dataInput = true;
      requestRender();}

}

其中 putNV21 用于将 NV21 中的 Y、U、V 数据分别取出

/**
* 将 NV21 数据的 Y、U、V 分量取出
*
* @param src    nv21 帧数据
* @param width  宽度
* @param height 高度
*/
private void putNV21(byte[] src, int width, int height) {

  int ySize = width * height;
  int frameSize = ySize * 3 / 2;

  // 取分量 y 值
  System.arraycopy(src, 0, yArray, 0, ySize);

  int k = 0;

  // 取分量 uv 值
  int index = ySize;
  while (index < frameSize) {vArray[k] = src[index++];
      uArray[k++] = src[index++];
  }
  yBuf.put(yArray).position(0);
  uBuf.put(uArray).position(0);
  vBuf.put(vArray).position(0);

}

在执行 requestRender 后,onDrawFrame 函数将被回调,在其中进行三个纹理的数据绑定并绘制

     @Override
     public void onDrawFrame(GL10 gl) {
      // 分别对每个纹理做激活、绑定、设置数据操作
      if (dataInput) {
          //y
          GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
          GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, yTexture[0]);
          GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D,
                  0,
                  0,
                  0,
                  frameWidth,
                  frameHeight,
                  GLES20.GL_LUMINANCE,
                  GLES20.GL_UNSIGNED_BYTE,
                  yBuf);

          //u
          GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
          GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, uTexture[0]);
          GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D,
                  0,
                  0,
                  0,
                  frameWidth >> 1,
                  frameHeight >> 1,
                  GLES20.GL_LUMINANCE,
                  GLES20.GL_UNSIGNED_BYTE,
                  uBuf);

          //v
          GLES20.glActiveTexture(GLES20.GL_TEXTURE2);
          GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, vTexture[0]);
          GLES20.glTexSubImage2D(GLES20.GL_TEXTURE_2D,
                  0,
                  0,
                  0,
                  frameWidth >> 1,
                  frameHeight >> 1,
                  GLES20.GL_LUMINANCE,
                  GLES20.GL_UNSIGNED_BYTE,
                  vBuf);
          // 在数据绑定完成后进行绘制
          GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
      }
  }

即可完成绘制。

四、加一层边框

有时候需求并不仅仅是圆形预览这么简单,我们可能还要为相机预览加一层边框

边框效果

一样的思路,我们动态地修改边框值,并进行重绘。
边框自定义 View 中的相关代码如下:

@Override
protected void onDraw(Canvas canvas) {super.onDraw(canvas);
    if (paint == null) {paint = new Paint();
        paint.setStyle(Paint.Style.STROKE);
        paint.setAntiAlias(true);
        SweepGradient sweepGradient = new SweepGradient(((float) getWidth() / 2), ((float) getHeight() / 2),
                new int[]{Color.GREEN, Color.CYAN, Color.BLUE, Color.CYAN, Color.GREEN}, null);
        paint.setShader(sweepGradient);
    }
    drawBorder(canvas, 6);
}


private void drawBorder(Canvas canvas, int rectThickness) {if (canvas == null) {return;}
    paint.setStrokeWidth(rectThickness);
    Path drawPath = new Path();
    drawPath.addRoundRect(new RectF(0, 0, getWidth(), getHeight()), radius, radius, Path.Direction.CW);
    canvas.drawPath(drawPath, paint);
}

public void turnRound() {invalidate();
}

public void setRadius(int radius) {this.radius = radius;}

五、完整 Demo 代码:

https://github.com/wangshengy…

使用 Camera API 和 Camera2 API 并选择最接近正方形的预览尺寸
使用 Camera API 并为其动态添加一层父控件,达到正方形预览的效果
使用 Camera API 获取预览数据,使用 OpenGL 的方式进行显示最后,给大家推荐一个好用的 Android 免费离线人脸识别的 sdk,可以和本文实现技术的完美结合:https://ai.arcsoft.com.cn/thi…

正文完
 0