Android多种方式实现相机圆形预览的示例代码

效果图如下:

一、为预览控件设置圆角

为控件设置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" rel="external nofollow" >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/wangshengyang1996/GLCameraDemo

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android自定义照相机Camera出现黑屏的解决方法

    本文实例讲述了Android自定义照相机Camera出现黑屏的解决方法.分享给大家供大家参考,具体如下: 对于一些手机,像HTC,当自定义Camera时,调用Camera.Parameters的 parameters.setPreviewSize(width, height)方法时,如果width和height为奇数情况下,则会出现黑屏现象,解决办法可参考SDK提供的ApiDemos中关于Camera的 例子: List<Size> sizes = parameters.getSupporte

  • Android自定义相机实现自动对焦和手动对焦

    Android自定义相机实现自动对焦和手动对焦: 不调用系统相机,因为不同的机器打开相机呈现的界面不统一也不能满足需求. 所以为了让程序在不同的机器上呈现出统一的界面,并且可以根据需求进行布局,做了此demo. 程序实现代码如下: import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.la

  • Android 实现调用系统照相机拍照和录像的功能

    本文实现android系统照相机的调用来拍照 项目的布局相当简单,只有一个Button: <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_heig

  • Android自定义照相机详解

    几乎每个APP都会用的相机功能,下面小编把内容整理分享到我们平台,供大家参考,感兴趣的朋友一起学习吧! 启动相机的两种方式 1.直接启动系统相机 <code class="hljs avrasm"> Intent intent = new Intent(); intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); startActivity(intent);</code> 或者指定返回图片的名称mCurrentPho

  • Android实现读取相机(相册)图片并进行剪裁

    我们先说一下思路,在android系统中就自带了图片剪切的应用,所以,我们只需要将我们获取到的相片传给图片剪切应用,再将剪切好的相片返回到我们自己的界面显示就ok了 在开发一些APP的过程中,我们可能涉及到头像的处理,比如从手机或者相册获取头像,剪裁成自己需要的头像,设置或上传头像等.网上一些相关的资料也是多不胜数,但在实际应用中往往会存在各种问题,没有一个完美的解决方案.由于近期项目的需求,就研究了一下,目前看来还没有什么问题. 这里我们只讨论获取.剪裁与设置,上传流程根据自己的业务需求添加.

  • Android启动相机拍照并返回图片

    具体实现过程请看下面代码: 简单的调用了一下系统的拍照功能 代码如下所示: //拍照的方法 private void openTakePhoto(){ /** * 在启动拍照之前最好先判断一下sdcard是否可用 */ String state = Environment.getExternalStorageState(); //拿到sdcard是否可用的状态码 if (state.equals(Environment.MEDIA_MOUNTED)){ //如果可用 Intent intent

  • Android调用相机并将照片存储到sd卡上实现方法

    Android中实现拍照有两种方法,一种是调用系统自带的相机,然后使用其返回的照片数据. 还有一种是自己用Camera类和其他相关类实现相机功能,这种方法定制度比较高,洗染也比较复杂,一般平常的应用只需使用第一种即可. 用Intent启动相机的代码: 复制代码 代码如下: Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); startActivityForResult(intent, 1);拍完照后就可以在onActivity

  • Android 调用系统相机拍摄获取照片的两种方法实现实例

    Android 调用系统相机拍摄获取照片的两种方法实现实例 在我们Android开发中经常需要做这个一个功能,调用系统相机拍照,然后获取拍摄的照片.下面是我总结的两种方法获取拍摄之后的照片,一种是通过Bundle来获取压缩过的照片,一种是通过SD卡获取的原图. 下面是演示代码: 布局文件: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http:

  • android中打开相机、打开相册进行图片的获取示例

    这里介绍在Android中实现相机调取.拍照片.获取照片.存储新路径等已经打开相册.选择照片等功能 首先看一下界面,很简单 配置读取内存卡和调用照相头的功能 <!-- 使用网络权限 --> <uses-permission android:name="android.permission.INTERNET"/> <!-- 写sd卡的权限 --> <uses-permission android:name="android.permis

  • Android实现从本地图库/相机拍照后裁剪图片并设置头像

    玩qq或者是微信的盆友都知道,这些聊天工具里都要设置头像,一般情况下大家的解决办法是从本地图库选择图片或是从相机拍照,然后根据自己的喜爱截取图片.上述过程已经实现好了,最后一步我加上了把截取好的图片在保存到本地的操作,来保存头像.为了大家需要,下面我们小编把完整的代码贴出来供大家参考. 先给大家展示效果图: 代码部分: 布局代码(其实就是两个按钮和一个ImageView来显示头像) <LinearLayout xmlns:android="http://schemas.android.co

  • Android实现调用系统图库与相机设置头像并保存在本地及服务器

    废话不多说了,直接给大家贴代码了,具体代码如下所述: /** * 1.实现原理:用户打开相册或相机选择相片后,相片经过压缩并设置在控件上,图片在本地sd卡存一份(如果有的话,没有则内部存储,所以还 * 需要判断用户是否挂载了sd卡),然后在服务器上存储一份该图片,当下次再次启动应用时,会默认去sd卡加载该图片,如果本地没有,再会去联网请求 * 2.使用了picasso框架以及自定义BitmapUtils工具类 * 3.记得加上相关权限 * <uses-permission android:nam

  • android 调用系统的照相机和图库实例详解

    android手机有自带的照相机和图库,我们做的项目中有时用到上传图片到服务器,今天做了一个项目用到这个功能,所以把我的代码记录下来和大家分享,第一次写博客希望各位大神多多批评. 首先上一段调用android相册和相机的代码: 复制代码 代码如下: Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);//调用android自带的照相机 photoUri = MediaStore.Images.Media.EXTERNAL_CON

  • Android中关于自定义相机预览界面拉伸问题

    关于自定义相机预览界面拉伸问题 1.导致主要变形的原因是Camera预览界面旋转的角度和摄像头挂载的角度不同导致的 2.我们的Activity设置的方向是竖屏,这是手机的自然方向 所以宽比高短 3.角度:所谓屏幕和摄像头的角度,指的是相对于自然方向旋转过的角度,根据旋转角度即可获知当前的方向 4.假如说:手机是竖屏的情况下, 自然角度为0,但是Camera逆时针旋转90度,那咱们设置顺时针旋转90度,就正常 .手机是横屏的情况下Camera返回为0度 ,如果设置顺时针旋转90度,就回旋转 怎么设

随机推荐