Android Camera瞭解一下
今天我們來了解下Android Camera的一些基本知識,包括下面一些內容
-
呼叫裝置的相機app拍攝照片
-
呼叫裝置的相機app拍攝視訊
-
通過相機api拍攝照片和視訊
1.呼叫裝置的相機app拍攝照片
先看下效果圖,拍攝可以返回縮圖和原圖,這裡看下返回原圖的效果,點選CAPTURE按鈕會呼叫裝置的camera app,拍攝後會返回Bitmap:

take pic.jpg
1.1 獲取相機特徵許可權
這個和平常的相機許可權不一樣,宣告改特徵是如果應用的主要特徵是跟相機攝像頭有關,那麼應用商店Google+Play/">Google Play會根據裝置是否有相機來決定是否下載該應用:
<manifest ... > <uses-feature android:name="android.hardware.camera" android:required="true" /> ... </manifest>
如果android:required設定為true,那麼如果裝置沒有相機就不會下載該應用;設定為false,會允許下載,這個時候程式員就需要在程式碼中增加是否有相機的特徵判斷:
hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
1.2 通過裝置camera拍攝照片
做Android的小夥伴們都知道要委託其他app完成某些工作需要通過系統的Intent來做。所以,我們通過裝置camera app也是需要Intent,包含三個步驟:
- 構造Intent
- 啟動相機app的activity
- 處理返回的資料
看下前面兩個步驟通過startActivityForResult的實現:
static final int REQUEST_IMAGE_CAPTURE = 1; private void dispatchTakePictureIntent() { Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (takePictureIntent.resolveActivity(getPackageManager()) != null) { startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE); } }
這裡有一點需要注意的,startActivityForResult之前需要增加一個判斷resolveActivity,它會返回第一個可以處理這個intent的activity,如果找不到可以處理這個intent的app,那麼我們的app就會crash,所以注意增加這個判斷。
第三點,處理返回的資料分成兩部分,一個是返回縮圖,畫素值小,另外一個就是返回原圖片,分別來看下這兩種情形。
1.3 返回縮圖
Camera app會在返回intent的extras中的“data”這個可以下帶回縮小版的Bitmap,看下程式碼:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) { Bundle extras = data.getExtras(); Bitmap imageBitmap = (Bitmap) extras.get("data"); mImageView.setImageBitmap(imageBitmap); } }
1.4 儲存原圖片
可以給Camera app一個路徑用於儲存原圖。如果不是敏感的圖片,Android系統推薦通過
getExternalStoragePublicDirectory(DIRECTORY_PICTURES)
放在這個路徑下的圖片可以被所有的app訪問到。當前前提是要宣告寫許可權:
<manifest ...> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> ... </manifest>
如果希望只有本app可以訪問到,可以通過下面路徑訪問到:
getExternalFilesDir(Environment.DIRECTORY_PICTURES)
所以可以抽離出一個函式返回儲存圖片的路徑:
String mCurrentPhotoPath; private File createImageFile() throws IOException { // Create an image file name String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); String imageFileName = "JPEG_" + timeStamp + "_"; File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES); File image = File.createTempFile( imageFileName,/* prefix */ ".jpg",/* suffix */ storageDir/* directory */ ); // Save a file: path for use with ACTION_VIEW intents mCurrentPhotoPath = image.getAbsolutePath(); return image; }
接著返回構造一個FileProvider,這樣Camera app才能訪問到:
private void dispatchTakeFullSizePicIntent() { Intent takePicIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (takePicIntent.resolveActivity(getPackageManager()) != null) { // Create the File where the photo should go File photoFile = null; try { photoFile = FileUtils.createImageFile(this); } catch (IOException e) { // Error occurred while creating the File e.printStackTrace(); } if (photoFile != null) { currentPhotoPath = photoFile.getAbsolutePath(); Uri photoURI = FileProvider.getUriForFile( this, getResources().getString(R.string.fileprovider_authority), photoFile ); takePicIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI); startActivityForResult(takePicIntent, REQUEST_TAKE_PHOTO); } } }
FileProvider需要在清單檔案中註冊,
<provider android:authorities="@string/fileprovider_authority" android:name="android.support.v4.content.FileProvider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths"/> </provider>
需要注意的是authorities需要和getUriForFile的第二個引數一樣,在meta-data標籤中通過在xml目錄下的file_paths路徑的xml檔案指定圖片的儲存路徑:
<?xml version="1.0" encoding="utf-8"?> <paths xmlns:android="http://schemas.android.com/apk/res/android"> <external-path name="my_images" path="Android/data/com.example.package.name/files/Pictures" /> </paths>
這個路徑其實就是上面的photoFile,也就是通過getExternalFilesDir(Environment.DIRECTORY_PICTURES)得到的。
如果要把上面儲存的圖片放到相簿可以通過下面的方式:
private void galleryAddPic() { Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); File f = new File(mCurrentPhotoPath); Uri contentUri = Uri.fromFile(f); mediaScanIntent.setData(contentUri); this.sendBroadcast(mediaScanIntent); }
2. 呼叫裝置的相機app拍攝視訊
跟上面呼叫裝置的相機app拍攝照片其實差不多,首先也是獲取相機特徵許可權,這個和上面是完全一樣的,不再重複說了。呼叫裝置的相機app拍攝視訊也是需要三個步驟,
- 構造Intent
- 啟動相機app的activity
- 處理返回的資料
首先看下構造intent和啟動:
static final int REQUEST_VIDEO_CAPTURE = 1; private void dispatchTakeVideoIntent() { Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); if (takeVideoIntent.resolveActivity(getPackageManager()) != null) { startActivityForResult(takeVideoIntent, REQUEST_VIDEO_CAPTURE); } }
和拍攝照片唯一不一樣的就是action,視訊的是MediaStore.ACTION_VIDEO_CAPTURE.
第三步就是處理返回的資料了:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == REQUEST_VIDEO_CAPTURE && resultCode == RESULT_OK) { Uri videoUri = data.getData(); videoView.setVideoURI(videoUri); if (videoView.getVisibility() == View.GONE) { videoView.setVisibility(View.VISIBLE); } videoView.start(); } }
通過intent的getData可以拿到視訊的uri,通過VideoView進行播放。
3.控制相機
一般建立自定義相機介面有下面幾個步驟:
- 確認裝置有相機和申請許可權
- 建立一個預覽介面,可以用TextureView或者SurfaceView/">SurfaceView
- 建立一個佈局,包含上面的預覽介面和控制介面UI,比如按鈕
- 建立UI和預覽介面之間的聯絡,比如點選拍攝,控制相機拍攝然後預覽介面顯示
- 儲存拍攝的檔案,照片或者視訊
- 釋放相機
3.1 確認裝置是否有相機
如前面第一部分說的,如果沒有在清單檔案中申明app需要相機,那麼就需要在程式碼中增加判斷裝置是否有相機,可以通過PackageManager.hasSystemFeature()方法判斷:
/** Check if this device has a camera */ private boolean checkCameraHardware(Context context) { if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){ // this device has a camera return true; } else { // no camera on this device return false; } }
在Android 2.3版本及以上,如果裝置有多個相機,可以通過Camera.getNumberOfCameras()確認
3.2 申請許可權
在Android 6.0以上需要執行時申請許可權,首先需要確認是否已授權:
ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED)
如果未授權,需要申請:
//ask for authorisation ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_PERMISSION_CODE);
最後一個引數是常量請求碼,在許可權反饋結果返回中需要用到,在onRequestPermissionsResult返回結果:
@Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { case REQUEST_PERMISSION_CODE: // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // permission was granted mPreview.startPreview(); } else { // permission denied, boo! Disable the // functionality that depends on this permission. finish(); } break; default: break; } }
3.3 獲取相機例項
確認過裝置有相機和app有相機許可權後需要獲取相機例項,其中id可以是前置相機(Camera.CameraInfo.CAMERA_FACING_FRONT)或者後置相機(Camera.CameraInfo.CAMERA_FACING_BACK):
private Camera safeCameraOpen(final int id) { Camera camera; try { releaseCameraAndPreview(); camera = Camera.open(id); } catch (Exception e) { e.printStackTrace(); camera = null; } latch.countDown(); return camera; } private void releaseCameraAndPreview() { if (mCamera != null) { mCamera.release(); mCamera = null; } }
open操作需要catch 異常,有可能其他app在使用相機或者相機裝置不存在,另外open操作是耗時操作,建議放線上程中。
3.4 獲取更多相機特徵
成功獲取相機例項後,可以通過Camera.getParameters()獲取更多相機的引數資訊,通過Camera.getCameraInfo()獲取相機是前置還是後置,和圖片的旋轉方向。
3.5 建立預覽介面
這裡通過TextureView建立預覽介面,TextureView需要實現介面TextureView.SurfaceTextureListener:
private void initTextureView() { if (null == textureView) { textureView = new TextureView(context); textureView.setKeepScreenOn(true); textureView.setSurfaceTextureListener(this); } LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER); addView(textureView, layoutParams); }
在系統建立好TextureView後會回撥onSurfaceTextureAvailable,在這裡就可以建立相機例項了:
@Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { initImg(); coverImg.setImageResource(R.drawable.second); final Camera[] camera = new Camera[1]; WorkerManager.getInstance().postTask(new Runnable() { @Override public void run() { camera[0] = safeCameraOpen(Camera.CameraInfo.CAMERA_FACING_BACK); } }); try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } setCamera(surface, camera[0]); }
其中coverImg是ImageView,在相機建立之前顯示一張封面圖用的。在非同步執行緒中開啟相機,然後把SurfaceTexture傳遞給相機就可以預覽到介面了。
在介面onSurfaceTextureDestroyed記得釋放相機:
@Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { stopPreviewAndFreeCamera(); return true; } /** * When this function returns, mCamera will be null. */ private void stopPreviewAndFreeCamera() { if (mCamera != null) { mCamera.stopPreview(); mCamera.release(); mCamera = null; }
3.6 建立一個佈局
佈局很簡單,新增一個容器用來放置上面的預覽介面CameraPreview:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center_horizontal" android:orientation="vertical"> <Button android:id="@+id/control" android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="@drawable/btn_bg" android:text="@string/capture_image" /> <com.example.juexingzhe.jueapp.view.CameraPreview android:id="@+id/cameraShower" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center_horizontal" android:scaleType="fitXY" /> </LinearLayout>
3.7 建立UI和預覽介面的關係
接下來就是建立UI,就是上面的按鈕,和預覽介面CameraPreview之間的關係,這裡就是點選按鈕拍攝一張照片:
private void initView() { mPreview = findViewById(R.id.cameraShower); controlBtn = findViewById(R.id.control); controlBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (mPreview != null) { mPreview.takePicture(); } } }); }
3.8 拍攝照片
上面點選按鈕呼叫CameraPreview的takePicture拍攝照片,這裡需要實現Camera.PictureCallback介面,在介面會回調回來二進位制資料:
/** * 拍照片 */ public void takePicture() { if (mCamera != null) { mCamera.takePicture(null, null, new Camera.PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { if (coverImg == null) { return; } if (data == null || data.length == 0) { return; } Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); if (bitmap != null) { coverImg.setVisibility(VISIBLE); coverImg.setImageBitmap(BitmapUtils.rotateBitmap(bitmap, (180 - rotationDigree))); stopPreviewAndFreeCamera(); } } }); } }
這裡直接decode二進位制資料生成Bitmap,然後給ImageView顯示。
4.總結
上面總結了Camera的一些用法,包括通過呼叫系統的相機app拍攝照片和視訊,自定義控制相機拍攝照片,其實還有一個自定義控制相機拍攝視訊,官方文件推薦結合用MediaRecorder錄製,但是現在比較常用的方式就是通過MediaCodec進行編碼,然後通過MediaMuxer混合成視訊格式,這個後面專門寫篇部落格分享。