Android——谷歌cameraview详解
Android的camera开发是经常能接触到的,但是由于多次迭代,camera的适配是一个很烦的事情,偶然的机会在github上看到google的一个关于camera的demo,这个demo兼容了各个版本的Android系统,谷歌出品必属精品。这篇文章就来研究一下这个demo。
github地址
这篇文章针对两类人,一类是想要直接使用camera,尽量快的完成开发,不关心内部原理的人;另一类就是想研究camera在各个版本中的适配以及他们的区别的人。我会针对不同人的需求来写这篇文章。
第一类
其实想要使用它是非常简单,首先将demo下载下来解压,目录如下:
library里面的就是我们需要的东西里,将libary作为module导入你的项目中就可以使用它了,使用起来也非常方便,demo中就有它的使用方法,我拿出来讲一下。
首先创建一个布局文件(也可以创建两个,因为相机横屏和竖屏时的显示方式不一定一样,这个根据需求来):
<?xml version="1.0" encoding="utf-8"?>
界面就是一个CameraView、一个显示菜单选项Toolbar和一个拍照用的FloatingActionButton。
再来看一下Activity:
public class MainActivity extends AppCompatActivity implements ActivityCompat.OnRequestPermissionsResultCallback { private static final String TAG = "MainActivity"; private static final int REQUEST_CAMERA_PERMISSION = 1; private CameraView mCameraView; //用于在子线程中处理图片数据 private Handler mBackgroundHandler; //拍照的点击事件 private View.OnClickListener mOnClickListener = new View.OnClickListener() { @Override public void onClick(View v) { switch (v.getId()) { case R.id.take_picture: if (mCameraView != null) { mCameraView.takePicture(); } break; } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mCameraView = (CameraView) findViewById(R.id.camera); //添加相机监听回调 if (mCameraView != null) { mCameraView.addCallback(mCallback); } FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.take_picture); if (fab != null) { fab.setOnClickListener(mOnClickListener); } Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setDisplayShowTitleEnabled(false); } } @Override protected void onResume() { super.onResume(); //检查权限,如果有权限就启动相机,没有就去请求权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { mCameraView.start(); } else { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION); } } @Override protected void onPause() { //关闭相机 mCameraView.stop(); super.onPause(); } @Override protected void onDestroy() { super.onDestroy(); if (mBackgroundHandler != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { mBackgroundHandler.getLooper().quitSafely(); } else { mBackgroundHandler.getLooper().quit(); } mBackgroundHandler = null; } } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case REQUEST_CAMERA_PERMISSION: if (permissions.length != 1 || grantResults.length != 1) { throw new RuntimeException("Error on requesting camera permission."); } if (grantResults[0] != PackageManager.PERMISSION_GRANTED) { Toast.makeText(this, R.string.camera_permission_not_granted, Toast.LENGTH_SHORT).show(); } // No need to start camera here; it is handled by onResume break; } } private Handler getBackgroundHandler() { if (mBackgroundHandler == null) { HandlerThread thread = new HandlerThread("background"); thread.start(); mBackgroundHandler = new Handler(thread.getLooper()); } return mBackgroundHandler; } //相机 监听回调事件 private CameraView.Callback mCallback = new CameraView.Callback() { @Override public void onCameraOpened(CameraView cameraView) { Log.d(TAG, "onCameraOpened"); } @Override public void onCameraClosed(CameraView cameraView) { Log.d(TAG, "onCameraClosed"); } @Override public void onPictureTaken(CameraView cameraView, final byte[] data) { Log.d(TAG, "onPictureTaken " + data.length); Toast.makeText(cameraView.getContext(), R.string.picture_taken, Toast.LENGTH_SHORT) .show(); getBackgroundHandler().post(new Runnable() { @Override public void run() { //在子线程中保存图片 File file = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "picture.jpg"); OutputStream os = null; try { os = new FileOutputStream(file); os.write(data); os.close(); } catch (IOException e) { Log.w(TAG, "Cannot write to " + file, e); } finally { if (os != null) { try { os.close(); } catch (IOException e) { // Ignore } } } } }); } };}
整个Activity的代码逻辑比较简单,首先在onCreate中初始相机和添加回调以及FloatingActionButton的点击事件监听,Toolbar的功能主要是对相机一些属性(闪光灯)之类的设置,这里就不讲那么多了,代码我也已经删掉了,需要这部分内容的可以直接去看github上下载的源码。然后在onResume中启动相机,在onPause中关闭相机。FloatingActionButton的点击事件会调用CameraView的拍照方法进行拍照,拍照后会在CameraView.Callback中回调拍照的结果以及数据。大致上就这么多,用起来还是比较简单的。
第二类
下面我们来深入研究一下这个开源项目的源码。首先说一下不同版本下相机开发的大致区别:
- Android 5.0以下的API为Camera 而 Android 5.0以上的API为Camera2,并且各大手机厂商对于Camera2的支持程度也不同。对于不支持Camera2的设备来说,需要降级使用Camera.
- 界面渲染主要涉及到SurfaceView 和 TextureView , 在4.0以上才能使用TextureView 。
SurfaceView不受View Hierarchy约束,拥有自己的Surface,可以理解为是另一个Window。因此一些View特性无法使用,但也因此不会影响主线程,可以放到其它的线程进行渲染,性能友好。
TextureView 则与普通的View类似,受View Hierarchy约束,相机发送过来的数据经由SurfaceTexture交接,让TextureView能以硬件加速渲染的方式存在视图树,也因此更耗费性能。
面对以上问题,CameraView 提出的方案如图:
在这个项目中所有的适配都由系统完成,用户只需要关心CameraView 这个类,那么这是怎么实现的呢?
先来看一下源码的目录结构:
我们来捋一下上面的文件逻辑,CameraViewImpl和PreviewImpl是两个抽象类,CameraViewImpl封装了所有关于相机的方法,Camera1和Camera2都继承这个类,实现其中的方法(当然他们的实现方式不一样,但是对外的接口是一样,也就是实现了一样的功能);PreviewImpl封装了所有关于界面的方法,TextureViewPreview和SurfaceViewPreview继承这个类,原理和CameraViewImpl一样。
在CameraViewImpl中会有一个PreviewImpl的实例,用来将CameraViewImpl中的画面绘制到PreviewImpl上,而CameraView中会有一个CameraViewImpl的实例,这样就可以在CameraView中控制相机,而且根据系统版本的不同CameraView会初始化不同的CameraViewImpl实例。大致过程就是这样,是不是没听懂,没关系,下面有源码分析。
先来看一下PreviewImpl这个类:
abstract class PreviewImpl { interface Callback { // surface发生了变动 void onSurfaceChanged(); } private Callback mCallback; // 预览视图高度 private int mWidth; // 预览视图宽度 private int mHeight; void setCallback(Callback callback) { mCallback = callback; } abstract Surface getSurface(); // 获取实际的渲染View abstract View getView(); // 输出源 abstract Class getOutputClass(); // 预览方向 abstract void setDisplayOrientation(int displayOrientation); // 渲染视图是否达到可用状态 abstract boolean isReady(); // 分发surface 的更变 protected void dispatchSurfaceChanged() { mCallback.onSurfaceChanged(); } // 主要是为了由SurfaceView渲染的情况 SurfaceHolder getSurfaceHolder() { return null; } // 主要是为了由TextureView渲染的情况 Object getSurfaceTexture() { return null; } // 设置缓冲区大小 void setBufferSize(int width, int height) { } void setSize(int width, int height) { mWidth = width; mHeight = height; } int getWidth() { return mWidth; } int getHeight() { return mHeight; }}
PreviewImpl的主要作用,是提供了必要信息使能接收CameraImpl给予的信息,并进行渲染。
看一下PreviewImpl的一个继承类SurfaceViewPreview:
class SurfaceViewPreview extends PreviewImpl { final SurfaceView mSurfaceView; SurfaceViewPreview(Context context, ViewGroup parent) { final View view = View.inflate(context, R.layout.surface_view, parent); mSurfaceView = (SurfaceView) view.findViewById(R.id.surface_view); final SurfaceHolder holder = mSurfaceView.getHolder(); //noinspection deprecation holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); holder.addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder h) { } @Override public void surfaceChanged(SurfaceHolder h, int format, int width, int height) { setSize(width, height); if (!ViewCompat.isInLayout(mSurfaceView)) { dispatchSurfaceChanged(); } } @Override public void surfaceDestroyed(SurfaceHolder h) { setSize(0, 0); } }); } @Override Surface getSurface() { return getSurfaceHolder().getSurface(); } @Override SurfaceHolder getSurfaceHolder() { return mSurfaceView.getHolder(); } @Override View getView() { return mSurfaceView; } @Override Class getOutputClass() { return SurfaceHolder.class; } @Override void setDisplayOrientation(int displayOrientation) { } @Override boolean isReady() { return getWidth() != 0 && getHeight() != 0; }}
这个类具体实现了PreviewImpl中的方法,在视图刷新的时候调用dispatchSurfaceChanged()方法,这个方法会回调到CameraView中。关于TextureViewPreview是一样的,只是具体的实现方法不一样,但对外的接口都是一样的。
再来看一下CameraViewImpl这个类:
abstract class CameraViewImpl { // 相机基础事件回调 protected final Callback mCallback; // 渲染视图 protected final PreviewImpl mPreview; CameraViewImpl(Callback callback, PreviewImpl preview) { mCallback = callback; mPreview = preview; } // 获取渲染视图 View getView() { return mPreview.getView(); } // 启动相机 abstract boolean start(); // 暂停相机 abstract void stop(); // 相机使用状态 abstract boolean isCameraOpened(); // 设置使用哪一个相机,简单如前置相机、后置相机 abstract void setFacing(int facing); // 获取当前相机标识 abstract int getFacing(); // 获取相机支持的预览比例 abstract Set getSupportedAspectRatios(); // 设置拍摄照片比例 abstract boolean setAspectRatio(AspectRatio ratio); // 获取相机当前摄照片比例 abstract AspectRatio getAspectRatio(); // 设置自动聚焦 abstract void setAutoFocus(boolean autoFocus); // 获取自动聚焦 abstract boolean getAutoFocus(); // 设置闪光状态 abstract void setFlash(int flash); // 获取闪光状态 abstract int getFlash(); // 获取静态图片,即拍照 abstract void takePicture(); // 设置相机方向 abstract void setDisplayOrientation(int displayOrientation); // 相机基础回调接口 interface Callback { // 相机已打开 void onCameraOpened(); // 相机已关闭 void onCameraClosed(); // 相机获取到静态图片 void onPictureTaken(byte[] data); }}
CameraViewImpl陈列了共性的可能的相机操作,挑一些做说明:
setFacing()
设置使用具体相机,简单的如前置相机、后置相机。在相机更变后,原本的预览视图可能因为Rotate而呈现出了不一样的视图,因此不仅需要更具需求切换到正确的相机,还需将预览视图进行矫正。
setAutoFocu()
一般来说,相机设备自动聚焦是默认开启的。当然如更多的聚焦模式如固定聚焦、景深、远景、微焦等也是可以另外支持的。当然,对于我等没有摄友的人来说,玩转不来。
setFlash()
闪光灯状态一般有如自动、关闭、拍照、防红眼等。
关于CameraViewImpl的继承类这里就不讲了,其实这里才是自定义相机的核心,后面如果有时间再来补充,这里留个坑。
我们着重来讲一下CameraView,CameraView是所有类的集合,封装就在这里体现。
由于代码太多,这里就不把所有的代码展示出来了,我们一个个的看。
首先看一下构造函数:
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); if (isInEditMode()){ mCallbacks = null; mDisplayOrientationDetector = null; return; } // Internal setup final PreviewImpl preview = createPreviewImpl(context); mCallbacks = new CallbackBridge(); if (Build.VERSION.SDK_INT < 21) { mImpl = new Camera1(mCallbacks, preview); } else if (Build.VERSION.SDK_INT < 23) { mImpl = new Camera2(mCallbacks, preview, context); } else { mImpl = new Camera2Api23(mCallbacks, preview, context); } // Attributes TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView, defStyleAttr, R.style.Widget_CameraView); mAdjustViewBounds = a.getBoolean(R.styleable.CameraView_android_adjustViewBounds, false); setFacing(a.getInt(R.styleable.CameraView_facing, FACING_BACK)); String aspectRatio = a.getString(R.styleable.CameraView_aspectRatio); if (aspectRatio != null) { setAspectRatio(AspectRatio.parse(aspectRatio)); } else { setAspectRatio(Constants.DEFAULT_ASPECT_RATIO); } setAutoFocus(a.getBoolean(R.styleable.CameraView_autoFocus, true)); setFlash(a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO)); a.recycle(); // Display orientation detector mDisplayOrientationDetector = new DisplayOrientationDetector(context) { @Override public void onDisplayOrientationChanged(int displayOrientation) { mImpl.setDisplayOrientation(displayOrientation); } }; }
看第9行的代码,这里通过createPreviewImpl方法创建了一个PreviewImpl实例,我们到这个方法里看一下:
@NonNull private PreviewImpl createPreviewImpl(Context context) { PreviewImpl preview; if (Build.VERSION.SDK_INT < 14) { preview = new SurfaceViewPreview(context, this); } else { preview = new TextureViewPreview(context, this); } return preview; }
其实就是根据android版本创建不同的view,这两个view都是我们之前创建的。
再看第10行代码,这里创建了一个CallbackBridge,我们来看一下这个CallbackBridge:
private class CallbackBridge implements CameraViewImpl.Callback { private final ArrayList mCallbacks = new ArrayList<>(); private boolean mRequestLayoutOnOpen; CallbackBridge() { } public void add(Callback callback) { mCallbacks.add(callback); } public void remove(Callback callback) { mCallbacks.remove(callback); } @Override public void onCameraOpened() { if (mRequestLayoutOnOpen) { mRequestLayoutOnOpen = false; requestLayout(); } for (Callback callback : mCallbacks) { callback.onCameraOpened(CameraView.this); } } @Override public void onCameraClosed() { for (Callback callback : mCallbacks) { callback.onCameraClosed(CameraView.this); } } @Override public void onPictureTaken(byte[] data) { for (Callback callback : mCallbacks) { callback.onPictureTaken(CameraView.this, data); } } public void reserveRequestLayoutOnOpen() { mRequestLayoutOnOpen = true; } }
这个Callback继承 CameraViewImpl.Callback ,主要用来监听相机的打开关闭拍照事件。
再看第11行,这里根据不同的版本号创建不同的Camera,然后把前面创建的Callback和preview传入其中,这样我们的Camera就创建成功了,接下来就是通过它进行各种操作。
构造函数里还初始化了方向,对焦模式,闪光灯等参数,这里就不展开讲了。
整体来看这个demo的架构还是比较简单明了的,不过看起来简单,想自己写出来还是挺难的,什么时候这样的架构能够信手拈来的时候技术也就到达一定的水平了。
就讲到这吧,其实根本就没有满足第二类人的需要,但是只要你搞懂了这个demo的架构,就完全有能力自己搞懂Camera,都在代码里,read the fuck code
更多相关文章
- Android(安卓)的几种布局方式及实践【转】
- Android基本组件之图像视图等余下组件(自用)
- Android视图加载到窗口的过程
- 技术揭秘:Android究竟比iOS差在哪里?
- 显卡上的战争,android倒戈,Vulkan崛起
- 【eoe教程】Android中自定义视图的绘制方法
- listview 分析
- Android(安卓)Mvp模式详解(Kotlin篇)
- android背景优化