Android&H5-js通过jsbridge调用安卓相机/相册/通讯录
阅读原文时间:2021年04月21日阅读:2

引文

是有这样一个需求 在vue里通过js可以直接调用安卓原生的照相机、相册和通讯录,并返回相应数据。当然前提是用webview来进行加载。

Android端处理

  • 在build.gradle引入(moudule:app)

    dependencies {
    compile 'com.github.lzyzsd:jsbridge:1.0.4'
    }

  • 在build.gradle引入(moudule:本项目)

    allprojects {
    repositories {
    google()
    jcenter()
    maven { url "https://jitpack.io" }
    }
    }

  • xml代码


    <com.github.lzyzsd.jsbridge.BridgeWebView
        android:id="@+id/web"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </com.github.lzyzsd.jsbridge.BridgeWebView>

  • java代码

    /**

    public class WebView extends AppCompatActivity {
    private final static int PHOTO_REQUEST = 100;
    private final static int PHOTO_STORY_REQUEST = 101;
    private final static int CONTACT_REQUEST = 102;
    private BridgeWebView mBridgeWebView;
    private Uri imageUri;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_web_view);
        findViewById();
        init();
    }
    
    private void findViewById() {
        mBridgeWebView = findViewById(R.id.web);
    }
    
    private void init() {
        WebSettings mSettings = mBridgeWebView.getSettings();
        mSettings.setUseWideViewPort(true);
        mSettings.setLoadWithOverviewMode(true);
        mSettings.setDomStorageEnabled(true);
        mSettings.setDefaultTextEncodingName("UTF-8");
        // 是否可访问Content Provider的资源,默认值 true
        mSettings.setAllowContentAccess(true);
        // 是否可访问本地文件,默认值 true
        mSettings.setAllowFileAccess(true);
        mSettings.setJavaScriptEnabled(true);
    mBridgeWebView.setWebChromeClient(new MyWebChromeClient());
    mBridgeWebView.loadUrl("http://10.3.102.129:8080/#/?time=1000015576");
    
    mBridgeWebView.setDefaultHandler(new BridgeHandler() {
        @Override
        public void handler(String data, CallBackFunction function) {
            function.onCallBack("DefaultHandler收到Web发来的数据,回传数据给你");
        }
    });
    
    mBridgeWebView.registerHandler("scan", new BridgeHandler() {
        @Override
        public void handler(String data, CallBackFunction function) {
            if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.M) {
                if (!checkPermission()) {
                    requestPermissions();
                } else {
                    takePhoto();//拍照逻辑
                }
            } else {
                takePhoto();//拍照逻辑
            }
            function.onCallBack("正在调用相机");
        }
    });
    
    mBridgeWebView.registerHandler("contact", new BridgeHandler() {
        @Override
        public void handler(String data, CallBackFunction function) {
            PhotoUtils.takeContact(WebView.this, CONTACT_REQUEST);
            function.onCallBack("正在调用通讯录");
        }
    });
    } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == PHOTO_REQUEST) { //照相回调 try { Bitmap bitmap = PhotoUtils.getBitmapFormUri(this, imageUri); mBridgeWebView.callHandler("scanResult", PhotoUtils.bitmapToBase64(bitmap), new CallBackFunction() { @Override public void onCallBack(String data) { Toast.makeText(WebView.this, data, Toast.LENGTH_SHORT).show(); } }); } catch (IOException e) { e.printStackTrace(); } } else if (requestCode == PHOTO_STORY_REQUEST) { //相册回调 Uri[] results = null; if (data == null) { results = null; } else { String dataString = data.getDataString(); ClipData clipData = data.getClipData(); if (clipData != null) { results = new Uri[clipData.getItemCount()]; for (int i = 0; i < clipData.getItemCount(); i++) { ClipData.Item item = clipData.getItemAt(i); results[i] = item.getUri(); } } if (dataString != null) { results = new Uri[]{Uri.parse(dataString)}; } } try { Bitmap bitmap = PhotoUtils.getBitmapFormUri(this, results[0]); mBridgeWebView.callHandler("scanResult", PhotoUtils.bitmapToBase64(bitmap), new CallBackFunction() { @Override public void onCallBack(String data) { Toast.makeText(WebView.this, data, Toast.LENGTH_SHORT).show(); } }); } catch (IOException e) { e.printStackTrace(); } } else if (requestCode == CONTACT_REQUEST) { //通讯录回调 if (data != null) { Uri uri = data.getData(); String phoneNum = null; String contactName = null; ContentResolver contentResolver = getContentResolver(); Cursor cursor = null; if (uri != null) { cursor = contentResolver.query(uri, new String[]{"display_name", "data1"}, null, null, null); } while (cursor.moveToNext()) { contactName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)); phoneNum = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)); } cursor.close(); // 把电话号码中的 - 符号 替换成空格 if (phoneNum != null) { phoneNum = phoneNum.replaceAll("-", " "); // 空格去掉 为什么不直接-替换成"" 因为测试的时候发现还是会有空格 只能这么处理 phoneNum = phoneNum.replaceAll(" ", ""); } ContactBean contactBean = new ContactBean(phoneNum, contactName); String str = GsonUtil.BeanToJson(contactBean); mBridgeWebView.callHandler("contactResult", str, new CallBackFunction() { @Override public void onCallBack(String data) { } }); } } } /** * 拍照 */ private void takePhoto() { File fileUri = new File(Environment.getExternalStorageDirectory().getPath() + "/" + SystemClock.currentThreadTimeMillis() + ".jpg"); if (!fileUri.getParentFile().exists()) { fileUri.getParentFile().mkdirs(); } imageUri = Uri.fromFile(fileUri); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //通过FileProvider创建一个content类型的Uri imageUri = FileProvider.getUriForFile(WebView.this, getPackageName() + ".fileProvider", fileUri); } PhotoUtils.takePicture(WebView.this, imageUri); } /** * 用户权限 申请 的回调方法 * * @param requestCode * @param permissions * @param grantResults */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == 101) { if (checkPermission()) { takePhoto(); } else { Toast toast = Toast.makeText(this, "没有相应权限!", Toast.LENGTH_SHORT); toast.show();
        }
    }
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    } /** * 是否有权限 * * @return */ private boolean checkPermission() { boolean haveCameraPermission = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED; boolean haveWritePermission = ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
    return haveCameraPermission &amp;&amp; haveWritePermission;
    } /** * 请求所需权限 */ @RequiresApi(api = Build.VERSION_CODES.M) private void requestPermissions() { ActivityCompat.requestPermissions(WebView.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 101); } public class MyWebChromeClient extends WebChromeClient { @Override public boolean onJsAlert(android.webkit.WebView view, String url, String message, final JsResult result) { //创建一个Builder来显示网页中的对话框 new AlertDialog.Builder(WebView.this) .setTitle("Alert对话框") .setMessage(message) .setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { result.confirm(); } }).setCancelable(false).show(); return true; } }

    }

  • java工具类

    /**

    public class PhotoUtils {
    private static final String TAG = "PhotoUtils";

    /**
     * 通讯录调用
     * @param activity 当前的activity
     * @param requestCode 调用通讯录的请求码
     */
    public static void takeContact(final Activity activity,final  int requestCode){
        //跳转到联系人界面
        Intent intent = new Intent();
        intent.setAction("android.intent.action.PICK");
        intent.addCategory("android.intent.category.DEFAULT");
        intent.setType("vnd.android.cursor.dir/phone_v2");
        activity.startActivityForResult(intent,requestCode);
    }
    
    /**
     * 拍照或选择相册
     *
     * @param activity    当前activity
     * @param imageUri    拍照后照片存储路径
     */
    public static void takePicture(final Activity activity, final Uri imageUri) {
        //调用系统相机
        AlertDialog.Builder alertDialog = new AlertDialog.Builder(activity);
        alertDialog.setTitle("选择");
        alertDialog.setItems(new CharSequence[]{"相机", "相册"}, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                if (which == 0) {
                    Intent intentCamera = new Intent();
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        //添加这一句表示对目标应用临时授权该Uri所代表的文件
                        intentCamera.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    }
                    intentCamera.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
                    //将拍照结果保存至photo_file的Uri中,不保留在相册中
                    intentCamera.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
                    if (activity != null) {
                        activity.startActivityForResult(intentCamera, 100);
                    }
                } else {
                    Intent i = new Intent(Intent.ACTION_GET_CONTENT);
                    i.addCategory(Intent.CATEGORY_OPENABLE);
                    i.setType("image/*");
                    activity.startActivityForResult(Intent.createChooser(i, "File Browser"), 101);
                }
            }
        });
        alertDialog.show();
    }
    
    /**
     * bitmap 转 base64
     * @param bitmap
     * @return
     */
    public static String bitmapToBase64(Bitmap bitmap) {
        String result = null;
        ByteArrayOutputStream baos = null;
        try {
            if (bitmap != null) {
                baos = new ByteArrayOutputStream();
                bitmap.compress(Bitmap.CompressFormat.JPEG, 20, baos);
            baos.flush();
            baos.close();
    
            byte[] bitmapBytes = baos.toByteArray();
            result = Base64.encodeToString(bitmapBytes, Base64.NO_WRAP);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (baos != null) {
                baos.flush();
                baos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    return result;
    } /** * @param activity 当前activity * @param requestCode 打开相册的请求码 */ public static void openPic(Activity activity, int requestCode) { Intent photoPickerIntent = new Intent(Intent.ACTION_GET_CONTENT); photoPickerIntent.setType("image/*"); activity.startActivityForResult(photoPickerIntent, requestCode); } /** * @param activity 当前activity * @param orgUri 剪裁原图的Uri * @param desUri 剪裁后的图片的Uri * @param aspectX X方向的比例 * @param aspectY Y方向的比例 * @param width 剪裁图片的宽度 * @param height 剪裁图片高度 * @param requestCode 剪裁图片的请求码 */ public static void cropImageUri(Activity activity, Uri orgUri, Uri desUri, int aspectX, int aspectY, int width, int height, 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.setDataAndType(orgUri, "image/*"); intent.putExtra("crop", "true"); intent.putExtra("aspectX", aspectX); intent.putExtra("aspectY", aspectY); intent.putExtra("outputX", width); intent.putExtra("outputY", height); intent.putExtra("scale", true); //将剪切的图片保存到目标Uri中 intent.putExtra(MediaStore.EXTRA_OUTPUT, desUri); intent.putExtra("return-data", false); intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); intent.putExtra("noFaceDetection", true); activity.startActivityForResult(intent, requestCode); } /** * 读取uri所在的图片 * * @param uri 图片对应的Uri * @param mContext 上下文对象 * @return 获取图像的Bitmap */ public static Bitmap getBitmapFromUri(Uri uri, Context mContext) { try {

    // Bitmap bitmap = MediaStore.Images.Media.getBitmap(mContext.getContentResolver(), uri);
    Bitmap bitmapFormUri = getBitmapFormUri(mContext, uri);
    return bitmapFormUri;
    } catch (Exception e) {
    e.printStackTrace();
    return null;
    }
    }

    /**
     * 通过uri获取图片并进行压缩
     *
     * @param uri
     */
    public static Bitmap getBitmapFormUri(Context ac, Uri uri) throws FileNotFoundException, IOException {
        InputStream input = ac.getContentResolver().openInputStream(uri);
        BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
        onlyBoundsOptions.inJustDecodeBounds = true;
        onlyBoundsOptions.inDither = true;//optional
        onlyBoundsOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional
        BitmapFactory.decodeStream(input, null, onlyBoundsOptions);
        input.close();
        int originalWidth = onlyBoundsOptions.outWidth;
        int originalHeight = onlyBoundsOptions.outHeight;
        if ((originalWidth == -1) || (originalHeight == -1)) {
            return null;
        }
        //图片分辨率以480x800为标准
        float hh = 800f;//这里设置高度为800f
        float ww = 480f;//这里设置宽度为480f
        //缩放比。由于是固定比例缩放,只用高或者宽其中一个数据进行计算即可
        int be = 1;//be=1表示不缩放
        if (originalWidth > originalHeight && originalWidth > ww) {//如果宽度大的话根据宽度固定大小缩放
            be = (int) (originalWidth / ww);
        } else if (originalWidth < originalHeight && originalHeight > hh) {//如果高度高的话根据宽度固定大小缩放
            be = (int) (originalHeight / hh);
        }
        if (be <= 0) {
            be = 1;
        }
        //比例压缩
        BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
        bitmapOptions.inSampleSize = be;//设置缩放比例
        bitmapOptions.inDither = true;//optional
        bitmapOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;//optional
        input = ac.getContentResolver().openInputStream(uri);
        Bitmap bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
        input.close();
        return compressImage(bitmap);//再进行质量压缩
    }
    
    /**
     * 质量压缩方法
     *
     * @param image
     * @return
     */
    public static Bitmap compressImage(Bitmap image) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//质量压缩方法,这里100表示不压缩,把压缩后的数据存放到baos中
        int options = 100;
        while (baos.toByteArray().length / 1024 > 100) {  //循环判断如果压缩后图片是否大于100kb,大于继续压缩
            baos.reset();//重置baos即清空baos
            //第一个参数 :图片格式 ,第二个参数: 图片质量,100为最高,0为最差  ,第三个参数:保存压缩后的数据的流
            image.compress(Bitmap.CompressFormat.JPEG, options, baos);//这里压缩options%,把压缩后的数据存放到baos中
            options -= 10;//每次都减少10
        }
        ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把压缩后的数据baos存放到ByteArrayInputStream中
        Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);//把ByteArrayInputStream数据生成图片
        return bitmap;
    }
    
    
    /**
     * @param context 上下文对象
     * @param uri     当前相册照片的Uri
     * @return 解析后的Uri对应的String
     */
    @SuppressLint("NewApi")
    public static String getPath(final Context context, final Uri uri) {
    final boolean isKitKat = Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.KITKAT;
    String pathHead = "file:///";
    // DocumentProvider
    if (isKitKat &amp;&amp; DocumentsContract.isDocumentUri(context, uri)) {
        // ExternalStorageProvider
        if (isExternalStorageDocument(uri)) {
            final String docId = DocumentsContract.getDocumentId(uri);
            final String[] split = docId.split(":");
            final String type = split[0];
            if ("primary".equalsIgnoreCase(type)) {
                return pathHead + Environment.getExternalStorageDirectory() + "/" + split[1];
            }
        }
        // DownloadsProvider
        else if (isDownloadsDocument(uri)) {
    
            final String id = DocumentsContract.getDocumentId(uri);
    
            final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
    
            return pathHead + getDataColumn(context, contentUri, null, null);
        }
        // MediaProvider
        else if (isMediaDocument(uri)) {
            final String docId = DocumentsContract.getDocumentId(uri);
            final String[] split = docId.split(":");
            final String type = split[0];
    
            Uri contentUri = null;
            if ("image".equals(type)) {
                contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
            } else if ("video".equals(type)) {
                contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
            } else if ("audio".equals(type)) {
                contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
            }
    
            final String selection = "_id=?";
            final String[] selectionArgs = new String[]{split[1]};
    
            return pathHead + getDataColumn(context, contentUri, selection, selectionArgs);
        }
    }
    // MediaStore (and general)
    else if ("content".equalsIgnoreCase(uri.getScheme())) {
        return pathHead + getDataColumn(context, uri, null, null);
    }
    // File
    else if ("file".equalsIgnoreCase(uri.getScheme())) {
        return pathHead + uri.getPath();
    }
    return null;
    } /** * Get the value of the data column for this Uri. This is useful for * MediaStore Uris, and other file-based ContentProviders. * * @param context The context. * @param uri The Uri to query. * @param selection (Optional) Filter used in the query. * @param selectionArgs (Optional) Selection arguments used in the query. * @return The value of the _data column, which is typically a file path. */ private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
    Cursor cursor = null;
    final String column = "_data";
    final String[] projection = {column};
    try {
        cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
        if (cursor != null &amp;&amp; cursor.moveToFirst()) {
            final int column_index = cursor.getColumnIndexOrThrow(column);
            return cursor.getString(column_index);
        }
    } finally {
        if (cursor != null) {
            cursor.close();
        }
    }
    return null;
    } /** * @param uri The Uri to check. * @return Whether the Uri authority is ExternalStorageProvider. */ private static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is DownloadsProvider. */ private static boolean isDownloadsDocument(Uri uri) { return "com.android.providers.downloads.documents".equals(uri.getAuthority()); } /** * @param uri The Uri to check. * @return Whether the Uri authority is MediaProvider. */ private static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); }

    }

前端处理

工具js

function setupWebViewJavascriptBridge(callback) {
  if (window.WebViewJavascriptBridge) {
    return callback(window.WebViewJavascriptBridge)
  }

  if (window.WVJBCallbacks) {
    return window.WVJBCallbacks.push(callback)
  }

  window.WVJBCallbacks = [callback];

  let WVJBIframe = document.createElement('iframe');

  WVJBIframe.style.display = 'none';

  WVJBIframe.src = 'https://__bridge_loaded__';

  document.documentElement.appendChild(WVJBIframe);

  setTimeout(() => {
    document.documentElement.removeChild(WVJBIframe)
  }, 0)

}

function connectWebViewJavascriptBridge(callback) {
  if (window.WebViewJavascriptBridge) {
    callback(WebViewJavascriptBridge)
  } else {
    document.addEventListener(
        'WebViewJavascriptBridgeReady'
        , function () {
          callback(WebViewJavascriptBridge)
        },
        false
    );
  }
}

// 判断安卓
function isAndroid() {
  const u = navigator.userAgent;
  return u.indexOf("Android") > -1 || u.indexOf("Linux") > -1;
}


export default {
  // 在需要调用客户端方法的组件中(事先需要与客户端同事约定好方法名)
  callhandler(name, data, callback) {
    if (isAndroid()) {
      connectWebViewJavascriptBridge((bridge) => {
        bridge.callHandler(name, data, callback);
      })
    } else {
      setupWebViewJavascriptBridge((bridge) => {
        bridge.callHandler(name, data, callback)
      })
    }

  },
  registerhandler(name, callback) {
    // 当客户端需要调用 js 函数时,事先注册约定好的函数即可
    if (isAndroid()) {
      connectWebViewJavascriptBridge((bridge) => {
        try {
          bridge.init(function (message, responseCallback) {
            // bridgeLog('默认接收收到来自Java数据: ' + message);
            var responseData = '默认接收收到来自Java的数据,回传数据给你';
            responseCallback(responseData);
          });
        }catch (e) {
          console.log(e);
        }
        bridge.registerHandler(name, (data, responseCallback) => {
          callback(data, responseCallback)
        });
      })
    } else {
      setupWebViewJavascriptBridge((bridge) => {
        bridge.registerHandler(name, (data, responseCallback) => {
          callback(data, responseCallback)
        })
      });
    }


  }


}

.vue

<template>
  <div>
    <button @click="clickUploadImg">获取图片</button>
    <button @click="click">测试</button>
    <button @click="contact">通讯录获取</button>
    <img style="width: 50px;height: 50px" :src="src">
    <p>{{text}}</p>
    <br>&nbsp;<br>


  </div>
</template>

<script>

  export default {
    name: '',
    components: {},
    data(){
      return{
        text:"通讯录text4",
        src:''
      }
    },
    mounted() {
      // 注册
      this.$bridge.registerhandler('scanResult', (data, responseCallback)=>{
        this.src = "data:image/jpg;base64,"+data;
        responseCallback(data);
      });
      this.$bridge.registerhandler('contactResult',(data,responseCallback)=>{
        this.text = data;
        responseCallback(data);
      });

    },
    methods: {
      contact(){
        const params = {
          data:'i'
        };
        this.$bridge.callhandler('contact',params,(data)=>{
          alert(data);
        })
      },
      clickUploadImg() {
        // eslint-disable-next-line no-unused-vars
        const params = {
          data: '1'
        };
        this.$bridge.callhandler('scan', params, (data)=>{
          this.text = JSON.stringify(data);
          alert(JSON.stringify(data));
          // 处理返回数据
        });

      },
      click(){

      }
    }
  }
</script>