买种子就上农科百蔬,农科百蔬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"/>
权限可能会造成相机闪退。