效果图如下:

一、为预览控件设置圆角

为控件设置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. arcgis python脚本工具实例教程—栅格范围提取至多边形要素类

    arcgis python脚本工具实例教程-栅格范围提取至多边形要素类 商务合作,科技咨询,版权转让:向日葵,135-4855_4328,xiexiaokui#qq.com 功能:提取栅格数据的范围, ...

  2. java使用ssh连接Linux并执行命令

     方式1:通过设置账号密码和链接地址 maven pom.xml配置: <dependency>         <groupId>com.jcraft</groupId ...

  3. (4)Flask项目模板渲染初体验

    一.准备静态资源 将项目使用到的静态资源拷贝到static目录 二.创建前台首页html 创建templates/home/home.html页面,内容包含导航和底部版权两部分,中间内容区域为模板标签 ...

  4. sysfile20191122

    ass_s_ccp_ft:-108; ass_s_ccp_all:-108; ass_tag_ft:-105; ass_tag_all:-105; rept_port:9000; Q_value:0. ...

  5. Cisco设备配置SSH登录

    一 试验拓扑 二 Server配置 ①配置hostname和domain name 因为rsa的秘钥是用hostname和domain name产生的 Router(config)#host Serv ...

  6. k8s记录-helm部署(九)

    helm 组件配置在 192.168.0.1 192.168.0.2 192.168.0.3 app 用户下操作tar xvf helm-v2.14.3-linux-amd64.tar.gzsudo ...

  7. node.js生成验证码及图片

    示例代码: var svgCaptcha = require('svg-captcha'); var fs = require('fs'); var codeConfig = { size: 5,// ...

  8. react 模板备份

    /** * Created by hldev on 17-6-14. * 上市公司详情展示界面 */ import React, {Component} from "react"; ...

  9. Redis 主从、哨兵Sentinel、Jedis

    Redis 主从.哨兵Sentinel.Jedis 2017年02月15日 15:52:48 有且仅有 阅读数 6183 文章标签: redis主从sentineljedis 更多 分类专栏: 7/1 ...

  10. 关于ios手机上拉和下拉到底部和顶部的橡皮筋问题

    之前遇到一个问题,就是写了一个正常的移动端首页,底部是导航,上面的是内容,在安卓手机是页面滑动到底部的时候,不会再滑动的,但是问题就出现在ios手机中.页面正常布局是,导航是position:fixe ...