There are questions remain, We'll search for the answers together. But one thing we known for sure,the future is not set!

【原创文章】最新版Android webview不响应input无法上传图片或拍照的解决办法

APP 百蔬君 12257℃ 已收录 7评论

买种子就上农科百蔬,农科百蔬APP一直在紧张的编写过程中,最近也已经完工,目前已经在应用宝、华为市场、小米市场、oppo市场,豌豆荚、PP助手、安智市场等一系列较为火爆的APP应用市场上线,有兴趣的朋友欢迎下载,https://www.nkbaishu.com/down/index.htm

在编写过程中遇到很多问题,其中一个较为重要的问题就是因为权限和系统原因,默认情况下android webview无法上传文件!也就是没有响应H5代码中的input上传文件方法,在pc段,当<input  type="file" >被激发的时候,会自动弹出对话框要求你选择图片,来上传文件。但是在android中却没有反应,那么很明显,这个问题需要解决。

农科百蔬一致秉承利人利己的原则,既然解决问题了,那么就要总结经验,提升自己的认识,同时也分享给朋友,给同行一个借鉴。

下面我把实现的方法详细的写出来,大家一起讨论。既然是APP,都具有相机功能,那么这个解决思路就不是简单的传照片了,而是两个部分,第一个,从手机中找到照片上传,第二个,拍照,同时由于流量的原因要需要对这个照片进行处理压缩,然后上传。

主要实现思路就是重写 webchromeClient 中的 openFileChooser 方法。我也是站在巨人的肩膀上解决这个问题的,主要参考了下面两篇很重要的文章:

Android webview上传图片(相册/相机)作者:IT一书生
https://www.jianshu.com/p/78412a9a9e83
适用于 Android 8.0 及以下全部版本的拍照示例,作者:不认真
https://blog.csdn.net/fengzhiqi1993/article/details/81216849

这两篇文章中的代码风格各异,从我个人的角度来说,比较喜欢“书生”的风格,代码简约,思路如行云流水,不拖沓,易懂,“不认真”的文章比较专业,很多过程都是单独拿出来作为过程或者函数,方便重复调用,读代码需要上下连贯查找。

但是”书生“的文章是写给android 6这个版本的,在android 7,8,9均无法正常启动相机功能!而”不认真“的文章主要针对的是在android 8.0版本下怎么实现拍照与裁剪,并不是针对webview。那么我的作用就是把”不认真“文章中拍照功能的实现与webview有机结合起来。

整个实现均以”书生“的demo为基础,其几个相关类为ZpImageUtils.java,ZpWebView.java,ZpWebChromeClient.java。

ZpImageUtils.java:主要实现图片压缩。

package 你的包名;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.os.Build;
import android.os.Environment;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

/**
* 处理图片工具类
*/
public class ZpImageUtils {

/**
* 压缩图片到指定宽高及大小
*/
public static File compressImage(Context context, String filePath, int destWidth, int destHeight, int imageSize) {
int degree = getBitmapDegree(filePath);

Bitmap bitmap = decodeSampledBitmapFromFile(filePath, destWidth, destHeight);
if (degree != 0) {
Matrix matrix = new Matrix();
matrix.postRotate(degree);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}
String destFilePath = getImageCheDir(context) + System.nanoTime() + ".jpg";
return saveImage(destFilePath, bitmap, imageSize);
}

private static String getImageCheDir(Context ctx) {
String filePath = getCacheDir(ctx);
return filePath + File.separator + "img" + File.separator;
}

private static String getCacheDir(Context context) {
File extCache = getExternalCacheDir(context);
boolean isSdcardOk = Environment.getExternalStorageState() == "mounted" || !isExternalStorageRemovable();
return isSdcardOk && null != extCache ? extCache.getPath() : context.getCacheDir().getPath();
}

@SuppressLint({"NewApi"})
public static boolean isExternalStorageRemovable() {
return Build.VERSION.SDK_INT >= 9 ? Environment.isExternalStorageRemovable() : true;
}

@SuppressLint({"NewApi"})
private static File getExternalCacheDir(Context context) {
if (hasExternalCacheDir()) {
return context.getExternalCacheDir();
} else {
String cacheDir = "/Android/data/" + context.getPackageName() + "/cache/";
return new File(Environment.getExternalStorageDirectory().getPath() + cacheDir);
}
}
private static boolean hasExternalCacheDir() {
return Build.VERSION.SDK_INT >= 8;
}
/**
* 保存图片到指定大小
*
* @param destFile 最终图片路径
* @param bitmap 原图
* @param imageSize 输出图片大小
*/
private static File saveImage(String destFile, Bitmap bitmap, long imageSize) {
// 判断父目录是否存在,不存在则创建
File result = new File(destFile.substring(0, destFile.lastIndexOf("/")));
if (!result.exists() && !result.mkdirs()) {
return null;
}

if (imageSize <= 0) {
imageSize = 400;
}

ByteArrayOutputStream bos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
int options = 100;
if (bos.toByteArray().length / 1024 > 1024) {
bos.reset();
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, bos);
options = 50;
}

while ((long) bos.toByteArray().length / 1024 > imageSize * 1.1D && options > 20) {
bos.reset();
options -= 10;
bitmap.compress(Bitmap.CompressFormat.JPEG, options, bos);
}

try {
FileOutputStream e = new FileOutputStream(destFile);
e.write(bos.toByteArray());
e.flush();
e.close();
} catch (IOException e1) {
e1.printStackTrace();
} finally {
bitmap.recycle();
bitmap = null;
}
return new File(destFile);
}

public static int getBitmapDegree(String path) {
short degree = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt("Orientation", 1);
switch (orientation) {
case 3:
degree = 180;
break;
case 6:
degree = 90;
break;
case 8:
degree = 270;
}
} catch (IOException var4) {
var4.printStackTrace();
}
return degree;
}

private static Bitmap decodeSampledBitmapFromFile(String path, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(path, options);
}

private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
int height = options.outHeight;
int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
int heightRatio = Math.round((float) height / (float) reqHeight);
int widthRatio = Math.round((float) width / (float) reqWidth);
inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}

}

ZpWebView.java:重写webview

package 你的包名;

import android.content.Context;
import android.util.AttributeSet;
import android.webkit.WebSettings;
import android.webkit.WebView;

public class ZpWebView extends WebView {

private ZpWebChromeClient webChromeClient;

public ZpWebView(Context context) {
super(context);
initWebView();
}

public ZpWebView(Context context, AttributeSet attrs) {
super(context, attrs);
initWebView();
}

public ZpWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initWebView();
}

private void initWebView() {
webChromeClient = new ZpWebChromeClient();
setWebChromeClient(webChromeClient);

WebSettings webviewSettings = getSettings();
// 不支持缩放
webviewSettings.setSupportZoom(false);
// 自适应屏幕大小
webviewSettings.setUseWideViewPort(true);
webviewSettings.setLoadWithOverviewMode(true);
String cacheDirPath = getContext().getFilesDir().getAbsolutePath() + "cache/";
webviewSettings.setAppCachePath(cacheDirPath);
webviewSettings.setAppCacheEnabled(true);
webviewSettings.setDomStorageEnabled(true);
webviewSettings.setAllowFileAccess(true);
webviewSettings.setAppCacheMaxSize(1024 * 1024 * 8);
webviewSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
}

public void setOpenFileChooserCallBack(ZpWebChromeClient.OpenFileChooserCallBack callBack) {
webChromeClient.setOpenFileChooserCallBack(callBack);
}

}

ZpWebChromeClient.java:重写WebChromeClient

package 你的包名;

import android.annotation.TargetApi;
import android.net.Uri;
import android.os.Build;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebView;

public class ZpWebChromeClient extends WebChromeClient {

private OpenFileChooserCallBack mOpenFileChooserCallBack;

// For Android < 3.0
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
openFileChooser(uploadMsg, "");
}

//For Android 3.0 - 4.0
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
if (mOpenFileChooserCallBack != null) {
mOpenFileChooserCallBack.openFileChooserCallBack(uploadMsg, acceptType);
}
}

// For Android 4.0 - 5.0
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
openFileChooser(uploadMsg, acceptType);
}

// For Android > 5.0
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
if (mOpenFileChooserCallBack != null) {
mOpenFileChooserCallBack.showFileChooserCallBack(filePathCallback, fileChooserParams);
}
return true;
}

public void setOpenFileChooserCallBack(OpenFileChooserCallBack callBack) {
mOpenFileChooserCallBack = callBack;
}

public interface OpenFileChooserCallBack {

void openFileChooserCallBack(ValueCallback<Uri> uploadMsg, String acceptType);

void showFileChooserCallBack(ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams);
}

}

1,添加权限和provider
\app\src\main\AndroidManifest.xml

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

添加provider

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="${applicationId}.UploadFileProvider"

    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />

这里需要注意的是authorities一定要与MainActivity中相关代码一致!

2,与AndroidManifest.xml中的resource一致,建立相关的xml文件。
\app\src\main\res\xml\file_paths.xml

<files-path
    name="my_images"
    path="images/" />

<files-path
    name="my_docs"
    path="docs/" />

<external-path
    name="external_files"
    path="."/>

external_files是必须要设置的,否则相机启动失败,没法读取路径!

3,配置菜单样式
\app\src\main\res\values\styles.xml

<style name="Dialog.NoTitle" parent="Theme.AppCompat.Dialog">
    <item name="android:windowNoTitle">true</item>
    <item name="android:windowFullscreen">true</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:windowIsTranslucent">true</item>
</style>

4,配置依赖包
\app\build.gradle

implementation 'com.android.support:design:28.0.0'
implementation 'com.android.support:appcompat-v7:28.0.0'

两个参数需要一致,否则报错

5,配置例外,避免重写的openFileChooser混淆

-keepclassmembers class * {
    @android.webkit.JavascriptInterface ;
}
#确保方法不被混淆
-keepclassmembers class * extends android.webkit.WebChromeClient{
 public void openFileChooser(...);
 }

6,给重写的webview设置样式
\app\src\main\res\layout\activity_main.xml

<你的包名.ZpWebView
android:id="@+id/webview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

这里包名不能省略

7,设置对话框样式
\app\src\main\res\layout\dialog_bottom_select_pictrue.xml

<View
    android:layout_width="match_parent"
    android:layout_height="1px"
    android:background="#e1e1e1" />

<TextView
    android:id="@+id/tv_select_pictrue_album"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingBottom="12dp"
    android:paddingLeft="16dp"
    android:paddingTop="12dp"
    android:text="相册"
    android:textColor="#999999"
    android:textSize="14sp" />

<View
    android:layout_width="match_parent"
    android:layout_height="1px"
    android:background="#e1e1e1" />

<TextView
    android:id="@+id/tv_select_pictrue_camera"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingBottom="12dp"
    android:paddingLeft="16dp"
    android:paddingTop="12dp"
    android:text="相机"
    android:textColor="#999999"
    android:textSize="14sp" />

<View
    android:layout_width="match_parent"
    android:layout_height="1px"
    android:background="#e1e1e1" />

<TextView
    android:id="@+id/tv_select_pictrue_cancel"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:paddingLeft="16dp"
    android:paddingTop="12dp"
    android:paddingBottom="12dp"
    android:text="取消"
    android:textColor="#999999"
    android:textSize="14sp" />

8,上面都是枝枝叶叶,下面修改主干
\app\src\main\java\com\nkbaishu\NKBaiShu\MainActivity.java

package 你的包名;
import android.annotation.TargetApi;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.MediaStore;
import android.support.design.widget.BottomSheetDialog;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;

public class MainActivity extends AppCompatActivity {

    public static final int REQUEST_SELECT_FILE_CODE = 100;
    private static final int REQUEST_FILE_CHOOSER_CODE = 101;
    private static final int REQUEST_FILE_CAMERA_CODE = 102;
    // 默认图片压缩大小(单位:K)
    public static final int IMAGE_COMPRESS_SIZE_DEFAULT = 400;
    // 压缩图片最小高度
    public static final int COMPRESS_MIN_HEIGHT = 900;
    // 压缩图片最小宽度
    public static final int COMPRESS_MIN_WIDTH = 675;

    private ValueCallback<Uri> mUploadMsg;
    private ValueCallback<Uri[]> mUploadMsgs;
    // 相机拍照返回的图片文件
    private File mFileFromCamera;
    private BottomSheetDialog selectPicDialog;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
    }

    private void initView() {
        ZpWebView webView = findViewById(R.id.webview);
        webView.setOpenFileChooserCallBack(new ZpWebChromeClient.OpenFileChooserCallBack() {
            @Override
            public void openFileChooserCallBack(ValueCallback<Uri> uploadMsg, String acceptType) {
                mUploadMsg = uploadMsg;
                showSelectPictrueDialog(0, null);
            }

            @Override
            public void showFileChooserCallBack(ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
                if (mUploadMsgs != null) {
                    mUploadMsgs.onReceiveValue(null);
                }
                mUploadMsgs = filePathCallback;
                showSelectPictrueDialog(1, fileChooserParams);
            }
        });
    }

    /**
     * 选择图片弹框
     */
    private void showSelectPictrueDialog(final int tag, final WebChromeClient.FileChooserParams fileChooserParams) {
        selectPicDialog = new BottomSheetDialog(this, R.style.Dialog_NoTitle);
        selectPicDialog.setOnDismissListener(new DialogInterface.OnDismissListener() {
            @Override
            public void onDismiss(DialogInterface dialogInterface) {
                if (mUploadMsgs != null) {
                    mUploadMsgs.onReceiveValue(null);
                    mUploadMsgs = null;
                }
            }
        });
        View view = LayoutInflater.from(this).inflate(R.layout.dialog_bottom_select_pictrue, null);
        // 相册
        TextView album = view.findViewById(R.id.tv_select_pictrue_album);
        // 相机
        TextView camera = view.findViewById(R.id.tv_select_pictrue_camera);
        // 取消
        TextView cancel = view.findViewById(R.id.tv_select_pictrue_cancel);

        album.setOnClickListener(new View.OnClickListener() {
            @TargetApi(Build.VERSION_CODES.LOLLIPOP)
            @Override
            public void onClick(View view) {
                if (tag == 0) {
                    Intent i = new Intent(Intent.ACTION_GET_CONTENT);
                    i.addCategory(Intent.CATEGORY_OPENABLE);
                    i.setType("*/*");
                    startActivityForResult(Intent.createChooser(i, "File Browser"), REQUEST_FILE_CHOOSER_CODE);
                } else {
                    try {
                        Intent intent = fileChooserParams.createIntent();
                        startActivityForResult(intent, REQUEST_SELECT_FILE_CODE);
                    } catch (ActivityNotFoundException e) {
                        mUploadMsgs = null;
                    }
                }
            }
        });
        camera.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                    showTakePicture();
            }
        });
        cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                selectPicDialog.dismiss();
            }
        });

        selectPicDialog.setContentView(view);
        selectPicDialog.show();
    }


    //获取拍照的权限
    private void showTakePicture() {
//        判断手机版本,如果低于6.0 则不用申请权限,直接拍照
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//7.0及以上
            if (ContextCompat.checkSelfPermission(this,
                    Manifest.permission.CAMERA)
                    != PackageManager.PERMISSION_GRANTED) {
                if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                        Manifest.permission.CAMERA)) {
                } else {
                    ActivityCompat.requestPermissions(this,
                            new String[]{Manifest.permission.CAMERA,Manifest.permission.WRITE_EXTERNAL_STORAGE,
                                    Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
                }
            }else{
                takeCameraPhoto();
            }
        }else{
            takeCameraPhoto();
        }

    }

    //权限申请的回调
    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == 1) {
            for (int i = 0; i < permissions.length; i++) {
                if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                    if (i == 0){
                        takeCameraPhoto();
                    }
                } else {
                    Toast.makeText(this, "" + "权限" + permissions[i] + "申请失败", Toast.LENGTH_SHORT).show();
                }
            }
        }
    }


    public void takeCameraPhoto() {
        if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
            Toast.makeText(this, "设备无摄像头", Toast.LENGTH_SHORT).show();
            return;
        }
       if (hasSdcard()) {
            String filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath();
            mFileFromCamera = new File(filePath, System.nanoTime() + ".jpg");
        }
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//临时权限flag
        Uri imgUrl;
        if (getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.M) {
            String authority = getPackageName()+".UploadFileProvider";
            imgUrl = FileProvider.getUriForFile(this, authority, mFileFromCamera);
        } else {
            imgUrl = Uri.fromFile(mFileFromCamera);
        }
        intent.putExtra(MediaStore.EXTRA_OUTPUT, imgUrl);
        startActivityForResult(intent, REQUEST_FILE_CAMERA_CODE);
    }


/*
     * 判断sdcard是否被挂载
     */
    public static boolean hasSdcard() {
        return Environment.getExternalStorageState().equals(
                Environment.MEDIA_MOUNTED);
    }


    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case REQUEST_SELECT_FILE_CODE:
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    if (mUploadMsgs == null) {
                        return;
                    }
                    mUploadMsgs.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));
                    mUploadMsgs = null;
                }
                break;
            case REQUEST_FILE_CHOOSER_CODE:
                if (mUploadMsg == null) {
                    return;
                }
                Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
                mUploadMsg.onReceiveValue(result);
                mUploadMsg = null;
                break;
            case REQUEST_FILE_CAMERA_CODE:
                takePictureFromCamera();
                break;
        }
    }

    /**
     * 处理相机返回的图片
     */
    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void takePictureFromCamera() {
        if (mFileFromCamera != null && mFileFromCamera.exists()) {
            String filePath = mFileFromCamera.getAbsolutePath();
            // 压缩图片到指定大小
            File imgFile = ZpImageUtils.compressImage(this, filePath, COMPRESS_MIN_WIDTH, COMPRESS_MIN_HEIGHT, IMAGE_COMPRESS_SIZE_DEFAULT);

            Uri localUri = Uri.fromFile(imgFile);
            Intent localIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, localUri);
            this.sendBroadcast(localIntent);
            Uri result = Uri.fromFile(imgFile);

            if (mUploadMsg != null) {
                mUploadMsg.onReceiveValue(Uri.parse(filePath));
                mUploadMsg = null;
            }
            if (mUploadMsgs != null) {
                mUploadMsgs.onReceiveValue(new Uri[]{result});
                mUploadMsgs = null;
            }
        }
    }
}

对于android7,8,9版本,如果没有分配动态权限,那么添加了<uses-permission android:name="android.permission.CAMERA"/>权限可能会造成相机闪退。

转载请注明:百蔬君 » 【原创文章】最新版Android webview不响应input无法上传图片或拍照的解决办法

喜欢 (73)or分享 (0)
发表我的评论
取消评论

请证明您不是机器人(^v^):

表情
(7)个小伙伴在吐槽
  1. 什么鬼
    匿名2019-03-20 15:48 回复
    • 什么什么鬼?
      匿名2019-03-26 19:30 回复
      • 啊啊啊啊
        匿名2019-04-20 17:17 回复
        • 测试 😮
          匿名2019-04-20 17:18 回复
    • 测试没法跑
      匿名2019-05-14 16:32 回复
  2. 什么东西 😮
    匿名2019-05-14 16:31 回复
  3. 什么鬼6666
    匿名2022-12-21 15:30 回复