一、目標
1、解決從純H5開發且帶拍照功能的App遷移至Android平臺時,保證拍照和裁剪功能正常;
2、解析實現過程中碰到的各種問題;
二、實現效果
爲了達成上述目標,先大體介紹下思路:
1)Android平臺下支持h5的input拍照標籤,但是需要在Android側對拍照過程做一些特殊處理才能正常獲取到此拍照圖片;
2)Android平臺存在版本碎片化問題,不同版本的拍照接口不同,需要做好兼容處理;
三、步驟
1、拍照需要用到Activity對象這個上下文,所以在初始化WebView時,需要把Activity對象(this)引用,傳遞給WebView,繼而傳遞給WebView中的WebCromeClient。如下是WebView中的代碼(WebChromeClient中的代碼詳見github):
/**
* 綁定activity
*
* @param activity
*/
public void bind(Activity activity)
{
// this.activity = activity;
this.webChromeClient.bind(activity);
}
2、Android中的拍照核心代碼:
1)WebChromeClient通用拍照代碼:
/**
* 拍照
*/
private void takePicture()
{
try
{
cameraFile = new File(path, SystemClock.currentThreadTimeMillis() + ".jpg");
Log.i(TAG, "camera file path=" + cameraFile.toString());
cameraUri = Uri.fromFile(cameraFile);
Log.i(TAG, "camera uri=" + cameraUri.toString());
PictureUtil.takePicture(this.activity, cameraUri, Constant.TAKE_PICTURE_CODE);
}
catch (Throwable t)
{
Log.e(TAG, "ERROR:", t);
}
}
2)上述只是封裝了一個Uri,真正的拍照代碼在PictureUtil中:
/**
* 拍照
*
* @param activity 當前activity
* @param imageUri 拍照後照片存儲路徑
* @param requestCode 調用系統相機請求碼
*/
public static void takePicture(Activity activity, Uri imageUri, int requestCode)
{
Log.i(TAG, "start to take a picture.");
// 調用系統相機
Intent cameraIntent = new Intent();
cameraIntent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
activity.startActivityForResult(cameraIntent, requestCode);
Log.i(TAG, "end to take a picture.");
}
這裏是調用了拍照的Activity,拍照完成後,需要在指定的Activity中,根據約定的requestCode來接收拍照完成後的結果。
3)上述拍照傳入的是MainActivity,其處理結果應在onActivityResult中,代碼如下:
@Override
protected void onActivityResult(int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
// 拍照
case Constant.TAKE_PICTURE_CODE: {
Log.i(TAG, "camera result:" + resultCode);
Log.i(TAG, "camera data:" + (data == null));
// 取消拍照
if (resultCode == Activity.RESULT_CANCELED) {
Log.i(TAG, "abort to take a picture.");
} else {
Log.i(TAG, "successfully to take a picture.");
}
this.webView.cropPicture(resultCode);
break;
}
}
}
4)上述Activity拍照成功的邏輯中,調用了圖片的裁剪功能。
WebChromeClient中的裁剪代碼如下:
/**
* 開始裁剪照片
*
* @param activity
* @param cameraUri
*/
private void cropPicture(Activity activity, Uri cameraUri)
{
File cropFile = new File(path, SystemClock.currentThreadTimeMillis() + ".jpg");
Log.i(TAG, "crop file path=" + cropFile.toString());
cropUri = Uri.fromFile(cropFile);
Log.i(TAG, "crop uri=" + cropUri.toString());
PictureUtil.Crop crop = new PictureUtil.Crop(1, 1, 250, 250);
PictureUtil.cropPicture(activity, cameraUri, cropUri, crop, Constant.CROP_PICTURE_CODE);
}
PictureUtil中真正的裁剪代碼爲:
/**
* 裁剪圖片
*
* @param activity 當前activity
* @param orgUri 剪裁原圖的Uri
* @param desUri 剪裁後的圖片的Uri
* @param crop:{aspectX X方向的比例;aspectY:Y方向的比例;width:剪裁圖片的寬度;height:剪裁圖片高度}
* @param requestCode 剪裁圖片的請求碼
*/
public static void cropPicture(Activity activity, Uri orgUri, Uri desUri, Crop crop, int requestCode)
{
Intent intent = new Intent("com.android.camera.action.CROP");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
{
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
intent.setDataAndType(orgUri, "image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", crop.getAspectX());
intent.putExtra("aspectY", crop.getAspectY());
intent.putExtra("outputX", crop.getWidth());
intent.putExtra("outputY", crop.getHeight());
intent.putExtra("scale", true);
// 將剪切的圖片保存到目標Uri中
intent.putExtra(MediaStore.EXTRA_OUTPUT, desUri);
// 取消人臉識別
intent.putExtra("noFaceDetection", true);
// true:返回bitmap,false:返回bitmap
intent.putExtra("return-data", true);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
activity.startActivityForResult(intent, requestCode);
}
裁剪的過程和拍照的過程類似,也會在指定的Activity中,根據約定的requestCode來接收裁剪完成後的結果。
6)上述裁剪傳入的Activity是MainActivity,其處理結果應在onActivityResult中,代碼如下:
@Override
protected void onActivityResult(int requestCode, final int resultCode, final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
switch (requestCode) {
case Constant.CROP_PICTURE_CODE: {
Log.i(TAG, "crop result:" + resultCode);
Log.i(TAG, "crop data:" + data);
Log.i(TAG, "crop data.getData:" + data.getData());
Bitmap photo = data.getParcelableExtra("data");
Log.i(TAG, "crop picture:" + photo);
// String base64Pic = PictureUtil.getPictureBase64(photo);
// Log.i(TAG, "base64Pic:" + base64Pic);
this.webView.cropCallback(resultCode);
break;
}
}
}
注意:此處的data中是可以拿到裁剪的Bitmap對象的,如果有這種訴求,可以在這裏加獲取的業務邏輯。
7)裁剪完成後,需要把處理完成的照片結果,返回給拍照接口中指定的回調對象,然後由Android返回給調用方(即我們的H5拍照組件),WebChromeClient中回調的核心代碼如下所示,其中valueCallback爲回調結果的處理接口,由Android系統接管。
/**
* 拍照異步回調
*
* @param resultCode
* @param uri
*/
public void cropCallback(int resultCode, Uri uri)
{
if (resultCode == Activity.RESULT_CANCELED)
{
takePictureCancel();
}
else
{
if (null != oldValueCallback)
{
oldValueCallback.onReceiveValue(cropUri);
}
else if (null != valueCallback)
{
valueCallback.onReceiveValue(new Uri[] {cropUri});
}
oldValueCallback = null;
valueCallback = null;
}
if (cameraFile != null)
{
Log.i(TAG, "delete cached camera file:" + cameraFile.getPath());
cameraFile.delete();
}
}
注意:上述代碼末尾有刪除拍照文件的操作,是因爲拍照和裁剪會分別產生圖片,而拍照的圖片大小又特別大(在我真機上測試佔有3M左右),所以裁剪完成後,需要把原圖從手機中刪掉。這個因具體業務而已。
3、上面一步詳細講了Android系統中的拍照和裁剪流程,下面再來列下Android版本碎片化的拍照入口,同樣在WebChromeClient中:
public void openFileChooser(ValueCallback<Uri> filePathCallback)
{
Log.d(TAG, "call openFileChooser01");
oldValueCallback = filePathCallback;
takePicture();
}
// For Android 3.0+
public void openFileChooser(ValueCallback filePathCallback, String acceptType)
{
Log.d(TAG, "call openFileChooser02");
oldValueCallback = filePathCallback;
takePicture();
}
// For Android 4.1
public void openFileChooser(ValueCallback<Uri> filePathCallback, String acceptType, String capture)
{
Log.d(TAG, "call openFileChooser03");
oldValueCallback = filePathCallback;
takePicture();
}
// For Android 5.0+
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams)
{
// super.onShowFileChooser(webView, filePathCallback, fileChooserParams);
Log.d(TAG, "call openFileChooser04");
valueCallback = filePathCallback;
takePicture();
return true;
}
經過上面3步,基本上就完成了拍照的全過程。再次重申,完整代碼見github。由於時間關係,本github代碼僅在本人真機和模擬器中驗證OK(Android 8.0+),低版本有待各位自己去驗證。
四、總結
1、拍照是當今手機一個非常核心的功能,掌握好這個知識點非常有用,網上資料也相當多,而且還出現了幾種不同的處理方案(有人說涉及拍照的權限升級,只能在高版本拍照時使用FileProvider得到的Uri,有人說必須設置XX權限);實際驗證卻並不 完全一致。至於原因,是不是當時的版本或者Android API差異導致的,無從得知;
2、在查找資料的過程中,發現有拍照和裁剪封裝成一個startActivityResult的,但由於時間問題,當時並沒有深入去研究,作爲一個遺留問題,留給愛思考愛動腦筋的你:)
五、參考資料
[1]Android7.0完美適配——FileProvider拍照裁剪全解析 - 擦肩的陽光的專欄
[2]Android webView拍照與展示相冊圖片 - 追風箏的擺渡人
[3]Android WebView實現選擇本地圖片拍照功能 - Ho博客
[4]深坑之Webview,解決H5調用android相機拍照和錄像 - villa_mou的博客
上一篇:Vue.js實戰——開發Android H5 App之Webview高級配置_13