開發Android Camera—使用Kotlin語言,完成第一個自定義相機
對於首次使用Kotlin語言開發,在網上苦於尋找不到Kotlin語言編寫的相機程式碼,故寫下這篇部落格。
好了,咱們進入主題
在Android 5.0(SDK 21)中,Google使用Camera2替代了Camera介面。Camera2在介面和架構上做了巨大的變動,但是基於眾所周知的原因,我們還必須基於 Android 4.+ 系統進行開發。本文介紹的是Camera介面開發及其使用方法,通過本文章,你將全面地學會Camera介面的開發流程。
開發步驟
- 新增相機相關許可權
- 通過Camera.open(int)獲得一個相機例項,也可以使用預設的Camera.open()
- 利用camera.parameters得到相機例項的預設設定Camera.parameters
- 如果需要的話,修改Camera.parameters並呼叫camera.parameters=Camera.Parameters來修改相機設定
- 呼叫camera.setDisplayOrientation(int)來設定正確的預覽方向
- 呼叫camera.setPreviewDisplay(SurfaceHolder)來設定預覽,如果沒有這一步,相機是無法開始預覽的
- 呼叫camera.startPreview()來開啟預覽,對於拍照,這一步是必須的
- 在需要的時候呼叫camera.takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback)來拍攝照片
- 拍攝照片後,相機會停止預覽,如果需要再次拍攝多張照片,需要再次呼叫camera.startPreview()來重新開始預覽
- 呼叫camera.stopPreview()來停止預覽
- 一定要在onPause()的時候呼叫camera.release()來釋放camera,在onResume中重新開始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) {}}}
}
}
相機預覽方向和預覽View的設定
不對相機預覽方向和應用方向設定,通常情況下得到的預覽結果是無法接受的。一般應用設定的方向為固定豎向,預覽設定旋轉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);
}
}
}
}