对于首次使用Kotlin语言开发,在网上苦于寻找不到Kotlin语言编写的相机代码,故写下这篇博客。
好了,咱们进入主题
在Android 5.0(SDK 21)中,Google使用Camera2替代了Camera接口。Camera2在接口和架构上做了巨大的变动,但是基于众所周知的原因,我们还必须基于 Android 4.+ 系统进行开发。本文介绍的是Camera接口开发及其使用方法,通过本文章,你将全面地学会Camera接口的开发流程。
在使用相机进行拍照,需要添加以下权限:
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
拍摄的照片需要存储到内存卡的话,还需要内存卡读写的权限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
根据拍照步骤就可以使用相机进行拍照,但是在使用过程中,有诸多需要注意的地方。
开启相机直接调用Camera.open(),理论上就可以得到一个相机实例。但是在开启相机前,最好check一下。虽然目前基本上所有的手机都是前后摄像头,但是也不排斥有些奇葩的手机,只有后摄像头,或者干脆没有摄像头的。所有在open一个camera前,先检查是否存在该id的camera。Camera.getNumberOfCameras()可以获得当前设备的Camera的个数N,N为0表示不支持摄像头,否则对应的Camera的ID就是0—(N-1)。
检查是否支持相机:
private fun isCameraAvailable(): Boolean {
val numberOfCameras = Camera.getNumberOfCameras()
if (numberOfCameras != 0) {
return true
}
return false
}
打开相机:
private fun getCamera(): Camera? {
var camera: Camera? = null
try {
camera = Camera.open()
} catch (e: Exception) {
e.printStackTrace()
}
return camera
}
Camera的Parameters提供了诸多属性,可以对相机进行多种设置,包括预览大小及格式、照片大小及格式、预览频率、拍摄场景、颜色效果、对焦方式等等,具体设置可参考官方手册。
拍照尤其需要注意的是对预览大小、照片大小以及对焦方式的设置。
在对预览大小、照片大小及对焦方式设置时,设置的值必须是当前设备所支持的。否则,预览大小和照片大小设置会无效,对焦方式设置会导致崩溃。它们都有相应的方法,获取相应的支持的列表。对应的依次为getSupportedPictureSizes(),getSupportedPictureSizes(),getSupportedFocusModes()。
相机设置代码:
private fun setParameters(camera: Camera?) {
camera?.let {
val params: Camera.Parameters = camera.parameters
params.pictureFormat = ImageFormat.JPEG
val size = Collections.max(params.supportedPictureSizes, object : Comparator<Camera.Size> {
override fun compare(lhs: Camera.Size, rhs: Camera.Size): Int {
return lhs.width * lhs.height - rhs.width * rhs.height
}
})
params.setPreviewSize(size.width, size.height);
params.focusMode = Camera.Parameters.FOCUS_MODE_AUTO
try {
camera.parameters = params; //在设置属性时,如果遇到未支持的大小时将会直接报错,故需要捕捉一样,做异常处理
} catch (e: Exception) {
e.printStackTrace()
try {
//遇到上面所说的情况,只能设置一个最小的预览尺寸
params.setPreviewSize(1920, 1080);
camera.parameters = params;
} catch (e1: Exception) {
//到这里还有问题,就是拍照尺寸的锅了,同样只能设置一个最小的拍照尺寸
e1.printStackTrace();
try {
params.setPictureSize(1920, 1080);
camera.parameters = params;
} catch (ignored: Exception) {}}}
}
}
不对相机预览方向和应用方向设置,通常情况下得到的预览结果是无法接受的。一般应用设置的方向为固定竖向,预览设置旋转90度即可。严谨点来说,预览方向的设置是根据当前window的rotation来设置的,即((WindowManager)displayView.getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation()的值。在Surface.ROTATION_0和Surface.ROTATION_180时,Camera设置displayOrientation为90,否则设置为0。
相机预览前,必须调用camera.setPreviewDisplay(SurfaceHolder)来设置预览的承载。SurfaceHolder一般取自SurfaceView、SurfaceTexture、TextureView等。一般情况下,如果不对显示的View大小做合理的设置,预览中的场景都会被变形。
添加SurfaceHolder的Callback监听
val holder = sv_camera.holder
holder?.addCallback(object : SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
}
override fun surfaceCreated(holder: SurfaceHolder?) {
setStartPreview(mCamera, holder);
}
})
设置预览的方向:
private fun setStartPreview(camera: Camera?, holder: SurfaceHolder?) {
camera?.let {
try {
camera.setPreviewDisplay(holder)
camera.setDisplayOrientation(Surface.ROTATION_90)
camera.startPreview()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
相机拍照时在预览时,调用takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback)(或其重载方法)来实现拍照的,其中第一个参数表示图像捕获时刻的回调。可以看到此方法的后三个参数类型是一样的,都是图像回调。分别表示原始图数据回调、展示图像数据的回调、JPEG图像数据的回调。图像回调中得到byte数组decode为image后,图片的方向都是摄像头原始的图像方向。
可以通过parameters.setRotation(int)来改变最后一个回调中图像数据的方向。个人推荐不设置,直接在回调中利用矩阵变换统一处理。因为利用parameters.setRotation(int)来旋转图像,在不同手机上会有差异。
与预览View设置类似,pictureSize设置的值,影响了最后的拍照结果,处理时需要对拍照的结果进行裁剪,使图片结果和在可视区域预览的结果相同。前摄像头拍摄的结果还需要做对称变换,以保证“所见即所得”。
拍照监听:
private fun taskPicture() {
mCamera?.autoFocus { success, camera ->
run {
mCamera?.takePicture(null, null, pictureCallback);
}
}
}
图片处理:
val pictureCallback = Camera.PictureCallback { data, camera ->
val pictureFile = File(Environment.getExternalStorageDirectory(), "gaozhongkui-" + System.currentTimeMillis() + ".jpg")
try {
val fos = FileOutputStream(pictureFile)
fos.write(data)
fos.close();
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
以下是完成的代码实例:
xml布局:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<SurfaceView
android:id="@+id/sv_camera"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/btn_capture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="拍照" />
</FrameLayout>
代码逻辑:
import android.graphics.ImageFormat
import android.hardware.Camera
import android.os.Bundle
import android.os.Environment
import android.support.v7.app.AppCompatActivity
import android.view.Surface
import android.view.SurfaceHolder
import kotlinx.android.synthetic.main.activity_main.*
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.*
import kotlin.Comparator
class MainActivity : AppCompatActivity() {
var mCamera: Camera? = null
val pictureCallback = Camera.PictureCallback { data, camera ->
val pictureFile = File(Environment.getExternalStorageDirectory(), "gaozhongkui-" + System.currentTimeMillis() + ".jpg")
try {
val fos = FileOutputStream(pictureFile)
fos.write(data)
fos.close();
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (isCameraAvailable()) {
mCamera = getCamera()
setParameters(mCamera);
}
initViewListener();
}
private fun initViewListener(){
val holder = sv_camera.holder
holder?.addCallback(object : SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
}
override fun surfaceCreated(holder: SurfaceHolder?) {
setStartPreview(mCamera, holder);
}
})
btn_capture.setOnClickListener {
taskPicture()
}
}
private fun isCameraAvailable(): Boolean {
val numberOfCameras = Camera.getNumberOfCameras()
if (numberOfCameras != 0) {
return true
}
return false
}
private fun getCamera(): Camera? {
var camera: Camera? = null
try {
camera = Camera.open()
} catch (e: Exception) {
e.printStackTrace()
}
return camera
}
fun releaseCamera() {
mCamera?.setPreviewCallback(null)
mCamera?.stopPreview()
mCamera?.release()
mCamera = null
}
private fun setStartPreview(camera: Camera?, holder: SurfaceHolder?) {
camera?.let {
try {
camera.setPreviewDisplay(holder)
camera.setDisplayOrientation(Surface.ROTATION_90)
camera.startPreview()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
private fun setParameters(camera: Camera?) {
camera?.let {
val params: Camera.Parameters = camera.parameters
params.pictureFormat = ImageFormat.JPEG
val size = Collections.max(params.supportedPictureSizes, object : Comparator<Camera.Size> {
override fun compare(lhs: Camera.Size, rhs: Camera.Size): Int {
return lhs.width * lhs.height - rhs.width * rhs.height
}
})
params.setPreviewSize(size.width, size.height);
params.focusMode = Camera.Parameters.FOCUS_MODE_AUTO
try {
camera.parameters = params;
} catch (e: Exception) {
e.printStackTrace()
try {
//遇到上面所说的情况,只能设置一个最小的预览尺寸
params.setPreviewSize(1920, 1080);
camera.parameters = params;
} catch (e1: Exception) {
//到这里还有问题,就是拍照尺寸的锅了,同样只能设置一个最小的拍照尺寸
e1.printStackTrace();
try {
params.setPictureSize(1920, 1080);
camera.parameters = params;
} catch (ignored: Exception) {
}
}
}
}
}
private fun taskPicture() {
mCamera?.autoFocus { success, camera ->
run {
mCamera?.takePicture(null, null, pictureCallback);
}
}
}
}