效果图如下:

一、为预览控件设置圆角

为控件设置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的预览尺寸为:1088x1088。

  • 在使用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的预览尺寸为:3024x3024、2976x2976、2160x2160、1936x1936。

只要我们选择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)
    在这里实现绘制操作。当我们设置的renderModeRENDERMODE_CONTINUOUSLY时,该函数将不断地执行;
    当我们设置的renderModeRENDERMODE_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世界坐标系
  • 着色器编写
   /**
* 顶点着色器
*/
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代表单个片元的颜色
  • 其他变量解释

    • ySampleruSamplervSampler
      分别代表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/wangshengyang1996/GLCameraDemo

  • 使用Camera API和Camera2 API并选择最接近正方形的预览尺寸
  • 使用Camera API并为其动态添加一层父控件,达到正方形预览的效果
  • 使用Camera API获取预览数据,使用OpenGL的方式进行显示

最后,给大家推荐一个好用的Android免费离线人脸识别的sdk,可以和本文实现技术的完美结合:

https://ai.arcsoft.com.cn/ucenter/resource/openPlatform/index.html?cnblogs

Android多种方式实现相机圆形预览的更多相关文章

  1. Android摄像头:只拍摄SurfaceView预览界面特定区域内容(矩形框)---完整(原理:底层SurfaceView+上层绘制ImageView)

    Android摄像头:只拍摄SurfaceView预览界面特定区域内容(矩形框)---完整实现(原理:底层SurfaceView+上层绘制ImageView) 分类: Android开发 Androi ...

  2. 谷歌发布 Android 8.1 首个开发者预览版,优化内存效率

    今晨,谷歌推出了 Android 8.1 首个开发者预览版,此次升级涵盖了针对多个功能的提升优化,其中包含对 Android Go (设备运行内存小于等于 1 GB)和加速设备上对机器学习的全新神经网 ...

  3. 基于“formData批量上传的多种实现” 的多图片预览、上传的多种实现

    前言 图片上传是web项目常见的需求,我基于之前的博客的代码(请戳:formData批量上传的多种实现)里的第三种方法实现多图片的预览.上传,并且支持三种方式添加图片到上传列表:选择图片.复制粘贴图片 ...

  4. Android实现本地图片选择及预览缩放效果仿春雨医生

    在做项目时常常会遇到选择本地图片的需求.曾经都是懒得写直接调用系统方法来选择图片.可是这样并不能实现多选效果.近期又遇到了,所以还是写一个demo好了.以后也方便使用.还是首先来看看效果 显示的图片使 ...

  5. Android 开发 Camera2开发_3_处理预览和拍照偏暗问题

    通过调整曝光解决 参考:https://stackoverflow.com/questions/28429071/camera-preview-is-too-dark-in-low-light-and ...

  6. android Camera拍照 及 MediaRecorder录像 预览图像差90度

    Camera拍照: 今天做照相机程序,结果写好了发现出问题了,预览的图像差90度.相关源代码如下: Camera.Parameters params = camera.getParameters(); ...

  7. Android 多种方式正确的加载图像,有效避免oom

    图像加载的方式: Android开发中消耗内存较多一般都是在图像上面,本文就主要介绍怎样正确的展现图像减少对内存的开销,有效的避免oom现象.首先我们知道我的获取图像的来源一般有三种源头:1.从网络加 ...

  8. Android 多种方式正确的载入图像,有效避免oom

    图像载入的方式:        Android开发中消耗内存较多一般都是在图像上面.本文就主要介绍如何正确的展现图像降低对内存的开销,有效的避免oom现象. 首先我们知道我的获取图像的来源一般有三种源 ...

  9. Android Studio 2.2新功能预览

    升级SDK可用Background 多加了个按钮,可用一边写代码一边下载SDK Instant Run 修改代码一秒启动 APK analyzer 分析任何的APK 查看APK下载包的大小,解压后的实 ...

随机推荐

  1. 手把手教你 GitLab 的安装及使用

    前言 新入职公司,发现公司还在使用落后生产工具 svn,由于重度使用过 svn 和 git ,知道这两个工具之间的差异,已经在使用 git 的路上越走越远. 于是,跟上级强烈建议让我在公司推行 git ...

  2. docker nginx安装

    安装nginx镜像docker search nginxdocker pull nginx 启动Nginx容器docker run -it -d --name n1 -v/home/n1/nginx. ...

  3. spark性能优化----缓存清除

    spark是一款优秀的框架,计算性能相当优异,已经发展成大数据主流计算引擎,在spark开发过程中有很多优化的点.其中去除重复计算是非常重要的.一般操作调用cache/persist,来缓存中间结果, ...

  4. Spring cloud微服务安全实战-7-3prometheus环境搭建

    Prmetheus 主要用来做来Metrics的监控和报警,这张图是官方的架构图. 这是他的核心 它的作用是根据我们的配置去完成数据的采集.服务的发现,以及数据的存储. 这是服务的发现,通过Servi ...

  5. matlab学习笔记8 基本绘图命令-特殊图形绘制

    一起来学matlab-matlab学习笔记8 基本绘图命令_3 特殊图形绘制 觉得有用的话,欢迎一起讨论相互学习~Follow Me 参考书籍 <matlab 程序设计与综合应用>张德丰等 ...

  6. PAT 甲级 1080 Graduate Admission (30 分) (简单,结构体排序模拟)

    1080 Graduate Admission (30 分)   It is said that in 2011, there are about 100 graduate schools ready ...

  7. BDC里的 BDC_OKCODE

      BDC OKCODE其实都是function code,对于处理一些BDC问题 非常有用,   列出一些知道的 OKCODE BDC_OKCODE 功能描述 =/00.  Enter = /8   ...

  8. MyBatis插入记录时返回主键id的方法

    有时候插入记录之后需要使用到插入记录的主键,通常是再查询一次来获取主键,但是MyBatis插入记录时可以设置成返回主键id,简化操作,方法大致有两种. 对应实体类: public class User ...

  9. SQL死锁情况汇总排查

    select dbname,entity_name,count(1) as locks from (SELECT request_session_id AS spid, DB_NAME(resourc ...

  10. [Oracle] - 查看数据库中每个表占用空间大小,及进行表压缩

    查询用户创建的表 select * from user_tab_comments; -- 查询本用户的表,视图等. select * from user_col_comments; -- 查询本用户的 ...