深坑之Webview,解决H5调用android相机拍照和录像

2023-05-16

最近在开发过程中遇到一个问题,主要是调用第三方的实名认证,需要拍照和录像;

办过支付宝大宝卡和腾讯的大王卡的都知道这玩意,办卡的时候就需要进行实名认证,人脸识别;

本来第三方平台(xxx流量公司)说的是直接用WebView加载这个H5界面就完事了,我心想这么简单,那不是分分钟的事,放着后面做(公司就我一个安卓,所以开发都是我说的算_,独立开发有的时候还是挺爽);

结果到项目快要上线的时候,只想说一句mmp,根本调不了相机,这个时候怎么搞,只有自己去实现了,才发现自己已经进了webview的深坑;

到处找资料,发现webview根本不能让h5自己调用,ios是可以的,项目经理就说是我的锅,真特么又一句mmp(关键是这个H5还特么不能改,不能提供给我调用的方法);

进入正题,首先来了解webview,这里我分享两篇大佬的博客
1,WebView开车指南
2,WebView详解
看完这两篇基本你已经可以随意操作webview了, 但是,当你调用相机的时候你才发现这只是入坑的开始

第一个坑

调用相机之后,文件回调不了,其实根本就是没有回调.这个时候你要去检查你的webview的Settings了关键代码,是否给足了权限;

 	  WebSettings settings = webView.getSettings();
        settings.setUseWideViewPort(true);
        settings.setLoadWithOverviewMode(true);
        settings.setDomStorageEnabled(true);
        settings.setDefaultTextEncodingName("UTF-8");
        settings.setAllowContentAccess(true); // 是否可访问Content Provider的资源,默认值 true
        settings.setAllowFileAccess(true);    // 是否可访问本地文件,默认值 true
        // 是否允许通过file url加载的Javascript读取本地文件,默认值 false
        settings.setAllowFileAccessFromFileURLs(false);
        // 是否允许通过file url加载的Javascript读取全部资源(包括文件,http,https),默认值 false
        settings.setAllowUniversalAccessFromFileURLs(false);
        //开启JavaScript支持
        settings.setJavaScriptEnabled(true);
        // 支持缩放
        settings.setSupportZoom(true);

其中settings.setDomStorageEnabled(true);这个方法当时我没加,血崩了一次;

第二个坑

WebChromeClient的openFileChooser()只调用了一次

首先了解为什么这个方法只调用了一次,看这篇博客就可
Android开发深入理解WebChromeClient之onShowFileChooser或openFileChooser使用说明
看了他基本你就了解怎么回事了,

简单来说就是每次调用onShowFileChooser或openFileChooser都要设置一个回调;不管你是不是选择了照片,如果没有就回传一个null

第三个坑
怎么区分是要调用相机是需要拍照还是录视频
1,这个时候你就要看WebViewClient的 shouldOverrideUrlLoading()方法了,我是通过拦截url来判断url里面是拍照还是录制视频来加一个flag,然后这样你调用你的相机的时候就可以分别处理了
2,另外如果实在拿不到这个flag,可以通过html的input标签来accept类型判断
在这里插入图片描述

在哪里拿呢

 //  5.0+ acceptType
   public boolean onShowFileChooser(WebView webView,
    ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
            String[] acceptTypes = fileChooserParams.getAcceptTypes();
}

  // For Android 3.0+acceptType
    public void openFileChooser(ValueCallback uploadMsg, String acceptType) {
    }

这样就可以拿到是accept进行调用了

第四个坑
android 7.0的FileProvider的坑
看洪阳大佬的这篇博客基本就了解了
Android 7.0 行为变更 通过FileProvider在应用间共享文件吧

最后附上我自己的代码吧

package com.sihaiwanlian.cmccnev.feature.verify.activitys;

import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ClipData;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.support.annotation.RequiresApi;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;

import com.orhanobut.logger.Logger;
import com.sihaiwanlian.cmccnev.R;
import com.sihaiwanlian.cmccnev.utils.PhotoUtils;

import java.io.File;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;

public class Certifica extends AppCompatActivity {
    private final static String TAG = "villa";
    @BindView(R.id.titleBar_iv_back)
    ImageView mTitleBarIvBack;
    @BindView(R.id.titleBar_btn_back)
    Button mTitleBarBtnBack;
    @BindView(R.id.titleBar_centerTV)
    TextView mTitleBarCenterTV;
    private WebView webView;
    private ValueCallback<Uri> mUploadMessage;
    private ValueCallback<Uri[]> mUploadCallbackAboveL;
    private final static int PHOTO_REQUEST = 100;
    private final static int VIDEO_REQUEST = 120;
    private final static String url = "your_url";
    private boolean videoFlag = false;
   
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_certifica);
        ButterKnife.bind(this);
        initToolBar();
        initWebView();
    }
  
    private void initToolBar() {
        mTitleBarCenterTV.setText("实名认证");
    }

    //初始化webView
    private void initWebView() {
        //从布局文件中扩展webView  
        webView = (WebView) this.findViewById(R.id.certifi_webview);
        initWebViewSetting();
    }

    //初始化webViewSetting
    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    private void initWebViewSetting() {
        WebSettings settings = webView.getSettings();
        settings.setUseWideViewPort(true);
        settings.setLoadWithOverviewMode(true);
        settings.setDomStorageEnabled(true);
        settings.setDefaultTextEncodingName("UTF-8");
        settings.setAllowContentAccess(true); // 是否可访问Content Provider的资源,默认值 true
        settings.setAllowFileAccess(true);    // 是否可访问本地文件,默认值 true
        // 是否允许通过file url加载的Javascript读取本地文件,默认值 false
        settings.setAllowFileAccessFromFileURLs(false);
        // 是否允许通过file url加载的Javascript读取全部资源(包括文件,http,https),默认值 false
        settings.setAllowUniversalAccessFromFileURLs(false);
        //开启JavaScript支持
        settings.setJavaScriptEnabled(true);
        // 支持缩放
        settings.setSupportZoom(true);
        //辅助WebView设置处理关于页面跳转,页面请求等操作
        webView.setWebViewClient(new MyWebViewClient());
        //辅助WebView处理图片上传操作
        webView.setWebChromeClient(new MyChromeWebClient());
        //加载地址
        webView.loadUrl(url);
    }

    @OnClick(R.id.titleBar_btn_back)
    public void onViewClicked() {
        if (webView.canGoBack()) {
            webView.goBack();// 返回前一个页面
        } else {
            finish();
        }
    }

    //自定义 WebViewClient 辅助WebView设置处理关于页面跳转,页面请求等操作【处理tel协议和视频通讯请求url的拦截转发】
    private class MyWebViewClient extends WebViewClient {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            Logger.e(url);
            if (!TextUtils.isEmpty(url)) {
                videoFlag = url.contains("vedio");
            }
            if (url.trim().startsWith("tel")) {//特殊情况tel,调用系统的拨号软件拨号【<a href="tel:1111111111">1111111111</a>】
                Intent i = new Intent(Intent.ACTION_VIEW);
                i.setData(Uri.parse(url));
                startActivity(i);
            } else {
                String port = url.substring(url.lastIndexOf(":") + 1, url.lastIndexOf("/"));//尝试要拦截的视频通讯url格式(808端口):【http://xxxx:808/?roomName】
                if (port.equals("808")) {//特殊情况【若打开的链接是视频通讯地址格式则调用系统浏览器打开】
                    Intent i = new Intent(Intent.ACTION_VIEW);
                    i.setData(Uri.parse(url));
                    startActivity(i);
                } else {//其它非特殊情况全部放行
                    view.loadUrl(url);
                }
            }
            return true;
        }
    }

   
    private Uri imageUri;

    //自定义 WebChromeClient 辅助WebView处理图片上传操作【<input type=file> 文件上传标签】
    public class MyChromeWebClient extends WebChromeClient {
        // For Android 3.0-  
        public void openFileChooser(ValueCallback<Uri> uploadMsg) {
            Log.d(TAG, "openFileChoose(ValueCallback<Uri> uploadMsg)");
            mUploadMessage = uploadMsg;
            if (videoFlag) {
                recordVideo();
            } else {
                takePhoto();
            }

        }

        // For Android 3.0+  
        public void openFileChooser(ValueCallback uploadMsg, String acceptType) {
            Log.d(TAG, "openFileChoose( ValueCallback uploadMsg, String acceptType )");
            mUploadMessage = uploadMsg;
            if (videoFlag) {
                recordVideo();
            } else {
                takePhoto();
            }
        }

        //For Android 4.1  
        public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
            Log.d(TAG, "openFileChoose(ValueCallback<Uri> uploadMsg, String acceptType, String capture)");
            mUploadMessage = uploadMsg;
            if (videoFlag) {
                recordVideo();
            } else {
                takePhoto();
            }
        }

        // For Android 5.0+  
        public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
            Log.d(TAG, "onShowFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture)");
            mUploadCallbackAboveL = filePathCallback;
            if (videoFlag) {
                recordVideo();
            } else {
                takePhoto();
            }
            return true;
        }
    }

    /**
     * 拍照
     */
    private void takePhoto() {
	 File fileUri = new File(Environment.getExternalStorageDirectory().getPath() + "/" + SystemClock.currentThreadTimeMillis() + ".jpg");
        imageUri = Uri.fromFile(fileUri);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            imageUri = FileProvider.getUriForFile(Certifica.this, getPackageName() + ".fileprovider", fileUri);//通过FileProvider创建一个content类型的Uri
        }
        PhotoUtils.takePicture(Certifica.this, imageUri, PHOTO_REQUEST);
    }

    /**
     * 录像
     */
    private void recordVideo() {
        Intent intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
        intent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 1);
        //限制时长
        intent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, 10);
        //开启摄像机
        startActivityForResult(intent, VIDEO_REQUEST);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        //如果按下的是回退键且历史记录里确实还有页面
        if ((keyCode == KeyEvent.KEYCODE_BACK) && webView.canGoBack()) {
            webView.goBack();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }


    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == PHOTO_REQUEST) {
            if (null == mUploadMessage && null == mUploadCallbackAboveL) return;
            Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
            if (mUploadCallbackAboveL != null) {
                onActivityResultAboveL(requestCode, resultCode, data);
            } else if (mUploadMessage != null) {
                mUploadMessage.onReceiveValue(result);
                mUploadMessage = null;
            }
        } else if (requestCode == VIDEO_REQUEST) {
            if (null == mUploadMessage && null == mUploadCallbackAboveL) return;

            Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
            if (mUploadCallbackAboveL != null) {
                if (resultCode == RESULT_OK) {
                    mUploadCallbackAboveL.onReceiveValue(new Uri[]{result});
                    mUploadCallbackAboveL = null;
                } else {
                    mUploadCallbackAboveL.onReceiveValue(new Uri[]{});
                    mUploadCallbackAboveL = null;
                }

            } else if (mUploadMessage != null) {
                if (resultCode == RESULT_OK) {
                    mUploadMessage.onReceiveValue(result);
                    mUploadMessage = null;
                } else {
                    mUploadMessage.onReceiveValue(Uri.EMPTY);
                    mUploadMessage = null;
                }

            }
        }
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void onActivityResultAboveL(int requestCode, int resultCode, Intent data) {
        if (requestCode != PHOTO_REQUEST || mUploadCallbackAboveL == null) {
            return;
        }
        Uri[] results = null;
        if (resultCode == Activity.RESULT_OK) {
            if (data == null) {
                results = new Uri[]{imageUri};
            } 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)};
            }
        }
        mUploadCallbackAboveL.onReceiveValue(results);
        mUploadCallbackAboveL = null;
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (webView != null) {
            webView.setWebViewClient(null);
            webView.setWebChromeClient(null);
            webView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
            webView.clearHistory();
//            ((ViewGroup) webView.getParent()).removeView(webView);
            webView.destroy();
            webView = null;
        }
    }
}  

这里面有个photoUtils,已经封装好了各种调用,简单好用

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

    /**
     * @param activity    当前activity
     * @param imageUri    拍照后照片存储路径
     * @param requestCode 调用系统相机请求码
     */
    public static void takePicture(Activity activity, Uri imageUri, int requestCode) {
        //调用系统相机
        Intent intentCamera = new Intent();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                intentCamera.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //添加这一句表示对目标应用临时授权该Uri所代表的文件
            }
            intentCamera.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
            //将拍照结果保存至photo_file的Uri中,不保留在相册中
            intentCamera.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
        if (activity!=null){
            activity.startActivityForResult(intentCamera, requestCode);
        }
    }

    /**
     * @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 >= Build.VERSION_CODES.KITKAT;
        String pathHead = "file:///";
        // DocumentProvider
        if (isKitKat && 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 && 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());
    }

}

欢迎大家扫描关注作者公众号,长期推送Android技术干货,感觉大家支持:

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

深坑之Webview,解决H5调用android相机拍照和录像 的相关文章

  • Nginx(二)初识配置文件【简单认识】

    版本 xff1a nginx x86 64 1 1 14 0 1 el7 4 ngx cat nginx conf 定义Nginx运行的用户和用户组 user nginx nginx进程数 xff0c 建议设置为等于CPU总核心数 work
  • MacBookM1远程连接云服务器安装boche+pintos

    1 前言 因为M1芯片的缘故 xff0c 没法直接在虚拟机 xff08 PD VMware xff09 上操作实验 xff0c 故出此下策 如果有条件还是希望uu们在装配有windows系统的笔记本上操作 此方法基于远程连接 xff0c 因
  • Vmware 实现自动\开机启动\关机挂起

    1 编写脚本 start bat ping 127 0 0 1 n 5 设置登录后 过几秒钟再启动虚拟机 34 C Program Files x86 VMware VMware Workstation vmrun exe 34 start
  • Python爬虫——BeautifulSoup的基本使用

    Python爬虫 BeautifulSoup的基本使用 安装beautifulsoup4模块 pip span class token function install span beautifulsoup4 1 创建BeautifulSo
  • python环境:本地有网服务器的python环境,迁移到离线服务器

    A服务器的虚拟环境 xff0c 迁移到离线服务器B xff08 已安装好anaconda xff09 设 xff1a A服务器的虚拟环境名称为 xff1a flask env 创建虚拟环境 在A服务器data01目录下 使用mkdir创建目
  • ROI管理与客户需求管理

    转自 xff1a http semwatch org 2010 02 roi management and client demand 作者 xff1a Gaoge 日期 xff1a 二月 26 2010 分类 xff1a 付费搜索SEM
  • 人脸识别过程

    做人脸识别的朋友很多不知道人脸识别过程 xff0c 人脸识别厂家人脸识别过程 人脸识别第一步先进行图像采集 xff0c 人脸定位 xff0c 特征提取 xff0c 特征对比 xff0c 人脸资料存储 xff0c 识别成功就可以开门 xff0
  • 个人博客的三种方式

    创建个人博客的三种方式 1 博客平台 简书 CSDN 知乎专栏等 特点 xff1a 简单 xff0c 可控性低 申请个账号就可以写 xff0c 其他的都不用管 限制多 xff0c 界面 排版 有没有广告自己说了不算 xff0c 什么能发什么
  • GitHub 简介

    用详细的图文对GitHub进行简单的介绍 git是一个版本控制工具 xff0c github是一个用git做版本控制的项目托管平台 主页介绍 xff1a overview xff1a 总览 相当于个人主页 repositories xff1
  • Markdown 编辑器推荐和常用语法介绍

    Markdown 是非常好用的文档编写方式 下面是我的 Markdown 学习总结 不求详尽 xff0c 只求简明 编辑器 xff1a 我目前使用的 Markdown 编辑器是 Joplin xff0c 感觉挺好用 我对编辑器的需求是 xf
  • 使用 Hexo 在 Github 上建博客

    先确认 git 与 npm 已经安装 xff0c 在终端输入以下命令 git version npm version 安装 hexo xff0c 在终端输入 npm install hexo cli g 安装过程中如果报错 解决方法 xff
  • ping命令的过程

    ping命令的过程 1 ping是什么 xff1f PING Packet Internet Groper xff0c 因特网包探索器 xff0c 用于测试网络连通性的程序 Ping发送一个ICMP Internet Control Mes
  • Hexo 更换主题

    更换 Hexo 主题非常容易 xff0c 只要在 themes 文件夹内 xff0c 新增一个任意名称的文件夹 xff0c 并修改 config yml 内的 theme 设定 xff0c 即可切换主题 具体步骤 xff1a 1 安装主题
  • Python 爬虫零基础教程(0):简介及准备

    其他的教程往往从语法开始 xff0c 而我们直接开始爬虫 xff0c 语法等知识边做边学 这第0篇我们简单介绍下爬虫和编程工具 爬虫是什么 爬虫是自动浏览 保存网页内容的程序或脚本 爬虫不同于黑客 xff0c 爬虫爬取的是允许访问的内容 工
  • 如何解决Unable to parse template "Interface"Error Message;

    Unable to parse template Interface Error Message This Template did not Produce a Java Class or an interface关于这个错误 xff0c
  • inflate函数使用总结

    inflate 两个参数和三个参数的区别 以前使用没有关注过 xff0c 因为觉得没报bug就行了 xff0c 两个三个参数无所谓 xff0c 经过导师提醒 xff0c 决定好好看看源码和相关知识 xff0c 总觉一下区别 xff0c 以免
  • 除了csdn网站打不开,一直刷新等待没反应,其他网站都能正常访问

    DNS错误 自动获取DNS服务器地址 清除DNS缓存信息 ipconfig span class token operator span flushdns 重置winsock 目录设置 netsh winsock reset
  • org.slf4j用法

    org slf4j用法 Scala 1 创建Logger对象 private val logger Logger 61 LoggerFactory getLogger classOf HttpBmlClient 2 打印错误信息 同时抛出异
  • 安装图形界面、VNCserver

    centos7 安装图形界面 xff1a 第一步 xff1a 安装Gnome包 在命令行下 输入下面的命令来安装Gnome包 yumgroupinstall 34 GNOMEDesktop 34 34 GraphicalAdministra
  • MySQL全量、增量备份与恢复的简单方法

    本文主要给大家介绍MySQL全量 增量备份与恢复的简单方法 xff0c 文章内容都是笔者用心摘选和编辑的 xff0c 具有一定的针对性 xff0c 对大家的参考意义还是比较大的 xff0c 下面跟笔者一起了解下MySQL全量 增量备份与恢复

随机推荐

  • windows安装zabbix代理

    一 关闭windows防火墙或者开通10050和10051端口 直接windows关闭防火墙或者在防火墙中放行10050和10051 二 xff0e 下载 安装并修改windows代理 1 下载zabbix agentd包 官网下载地址 x
  • android8.0 Fingerprint 指纹输错5次后亮屏显示错误信息

    当有指纹解锁时 xff0c 会执行AuthenticationClient java gt onAuthenticated 一直在监听解锁行为 64 Override public boolean onAuthenticated int f
  • secureCRT 抓取串口数据

    language 61 34 VBScript 34 interface 61 34 1 0 34 Dim outputFile fout Dim outputPath Dim user outputPath 61 34 D out txt
  • 2022年编程语言热度排行榜来啦,快来看看你学习的语言排第几

    提示 xff1a 文章写完后 xff0c 目录可以自动生成 xff0c 如何生成可参考右边的帮助文档 前言 一直以来 xff0c 编程语言都是程序员非常关注的话题 年末将至 xff0c 是否会有程序员发出疑问 2022 年行业需求最大的编程
  • LibEvent-Demo

    libevent test cpp 定义控制台应用程序的入口点 include 34 stdafx h 34 pragma comment lib 34 ws2 32 lib 34 pragma comment lib 34 wsock32
  • 【数据挖掘】DBSCAN聚类算法(python实现)

    一 python代码 39 39 39 Author Vici date 2020 5 14 39 39 39 import math 39 39 39 Point类 xff0c 记录坐标x xff0c y和点的名字id 39 39 39
  • ubuntu中安装rpm格式的软件包

    ubuntu的软件包格式是deb xff0c 如果要安装rpm的包 xff0c 则要先用alien把rpm转换成deb zhhLinux联盟 sudo apt get install alien alien默认没有安装 xff0c 所以首先
  • SQLite3在windows下的配置(链接VC++或者VS)

    一 SQLite3 简介 SQLite3 是一个开源免费的嵌入式关系数据库 xff0c 它在 2000 年由 D Richard Hipp 发布 xff0c 它不像大型数据库管理系统 xff0c 占用系统大量资源 SQLite3 是用 C
  • 电脑正常联网,提示无法登录微信

    事故起因 xff1a 因为工作需要安装一款软件 xff0c 安装途中提示有风险直接忽略了该风险 后期卸掉该软件 xff0c 然后无法登录微信 QQ easyconnect等软件 xff0c 但是浏览器可以正常访问 登录微信出现的界面是 xf
  • Ubuntu-GNOME 16.04 LTS更换主题

    1 安装gnome shell扩展插件 进入https extensions gnome org 先将gnome安装插件安装到火狐浏览器上 xff0c 而后查找插件 User Themes by fmuellner 如果无法选择比如题头报错
  • 图片服务器搭建 ftp上传http协议读取图片

    怎样在Win7系统中搭建Web服务器 详见百度搭建教程web服务器搭建 web服务器搭建 搭建好服务器以后配置 controller层 span class hljs javadoc 上传头像 span span class hljs an
  • SVM&TSVM&LSA(I)→PLSA(I)→LDA→HDP

    SVM amp TSVM amp LSA I PLSA I LDA HDP SVM xff08 用于监督学习 xff09 参考文章 xff1a SVM xff08 支持向量机 xff09 详解 通俗来讲 xff0c SVM是一种二类分类模型
  • adb源码分析

    ADB是Android debug bridge的缩写 xff0c 它使用PC机可以通过USB或网络与android设备通讯 adb的源码位于system core adb目录下 xff0c 先来看下编译脚本Android mk xff1a
  • android源码编译笔记--踩坑

    错误 xff1a ninja build stopped subcommand failed 解决 xff1a 打开 prebuilts sdk tools jack admin 找到 JACK SERVER COMMAND 61 34 j
  • 这个交互也太炸裂了趴

    动画是为一个app创造出色用户体验的重要组成部分 它的关键挑战是向用户解释应用程序的逻辑 xff0c 但是常见的错误是鲁莽地使用动画 xff0c 从而否定了改善用户体验的整个观点 为了使应用出色而不仅仅是出色或平庸 xff0c 动画必须正确
  • 在ES6的语法中如何给数组去重

    这是从数组中筛选出重复项并仅返回唯一值的三种方法 我最喜欢使用Set xff0c 因为它最短 xff0c 最简单 xff1b set Set是ES6中引入的新数据对象 因为Set仅允许您存储唯一值 传递数组时 xff0c 它将删除所有重复值
  • 笔记啊啊啊啊

    判断某个字符串中是否其他某些字符串 span class token keyword const span deviceName span class token operator 61 span e span class token pu
  • android 高德地图之poi搜索功能的实现

    二话不多说 先看效果 这个功能我是用Fragmentdialog里面做的 也遇到不少坑 第一 就是设置背景的drawable为纯白色导致键盘弹出的时候 recyclerview的布局被顶上去导致出现白色布局 有点扎眼 最后改成了设置为和背景
  • android之 h5调用系统相机和相册并显示

    先上html界面的代码 放在assets里面就可以了 我也不太会html 所以随便写了点 span class hljs doctype lt doctype html gt span span class hljs tag lt span
  • 深坑之Webview,解决H5调用android相机拍照和录像

    最近在开发过程中遇到一个问题 主要是调用第三方的实名认证 需要拍照和录像 办过支付宝大宝卡和腾讯的大王卡的都知道这玩意 办卡的时候就需要进行实名认证 人脸识别 本来第三方平台 xxx流量公司 说的是直接用WebView加载这个H5界面就完事