Vue.js实战——H5拍照迁移至Android App_14

一、目标

    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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章