Android webView包装WebAPP

学向勤中得,萤窗万卷书。这篇文章主要讲述Android webView包装WebAPP相关的知识,希望能为你提供帮助。

前言 android webView 兼容体验真的差到了极点!!
前一阵子,老板要将 WebAPP 放到 Android 和 ios 里面,而我因为以前做过安卓,所以这方面就由我来打包,
原理是很简单的,就是打开 APP 的时候用 webView 加载网站的网址,这样服务器一次更新,就能更新微信版, iOS 版和 Android 版;
首先我要说一句,如果你的 WebAPP 里面有文件上传,并且想要完全兼容,那么就别用原生的 WebAPP, 后面我会写一个关于 crossWalk 的博客,不过在此之前,我先记录下我所经历的一些坑,我的工具使用的是 Android studio;
创建一个项目,这个我就不说了,网上很多教程;
首先在 app/src/main/AndroidManifest.xml 里添加权限:
注意本文代码中的" ..." 都代表省略的代码
< manifest ...> < uses-permission android:name=" android.permission.INTERNET" /> < uses-permission android:name=" android.permission.WRITE_EXTERNAL_STORAGE" /> < uses-permission android:name=" android.permission.READ_EXTERNAL_STORAGE" /> < application ... < /application> < /manifest>

  • 第一个是允许访问网络连接;
  • 第二个是允许程序写入外部存储,如SD卡上写文件;
  • 第三个是允许应用程序从外部存储读取;
再是 app/src/main/res/layout/activity_main.xml 添加:
< WebView android:id=" @+id/local_webview" android:layout_width=" match_parent" android:layout_height=" match_parent" android:visibility=" gone" />

MainActivety.java:
private WebView webview; //... protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (Build.VERSION.SDK_INT > = Build.VERSION_CODES.KITKAT) { WebView.setWebContentsDebuggingEnabled(true); }webview = findViewById(R.id.local_webview); WebSettings settings = webview.getSettings(); loading = findViewById(R.id.loadView); settings.setjavascriptEnabled(true); //必须settings.setCacheMode(WebSettings.LOAD_DEFAULT); //关闭webview中缓存 settings.setRenderPriority(WebSettings.RenderPriority.HIGH); //提高渲染的优先级 settings.setUseWideViewPort(true); //WebView是否支持html的“viewport”标签或者使用wide viewport。 settings.setAllowContentAccess(true); //是否允许在WebView中访问内容URL settings.setBuiltInZoomControls(true); //是否使用其内置的变焦机制 settings.setJavaScriptCanOpenWindowsAutomatically(true); //是否允许自动打开弹窗 settings.setDomStorageEnabled(true); //是否开启DOM存储API权限webview.loadUrl(" http://www.baidu.com" ); webview.setWebChromeClient(new WebChromeClient() { @Override public void onProgressChanged(WebView view, int newProgress) { Log.d(" 加载" , " on page progress changed and progress is " + newProgress); //... }}webview.setWebViewClient(new WebViewClient() { @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { super.onReceivedError(view, errorCode, description, failingUrl); // 加载网页失败时处理如: view.loadDataWithBaseURL(null, " < span> 页面加载失败,请确认网络是否连接< /span> " , " text/html" , " utf-8" , null); }@Override public void onPageFinished(WebView view, String url) { if (!webview.getSettings().getLoadsImagesAutomatically()) { webview.getSettings().setLoadsImagesAutomatically(true); } Log.d(" 加载" , " end " ); }}); }

这是一个比较简单的 webView 例子,这里有几点需要说下:
  1. 关于WebSettings:
    1.1 需要运行 js 的网页都需要此设置:setJavaScriptEnabled
    1.2 关于setCacheMode,尽量不要设置 LOAD_CACHE_ONLY 该值,设置这个值会在 webkit 类型浏览器对短时间内的 ajax 访问产生Provisional headers are shown问题;
    1.3 关于 AllowFileAccess 一般默认值就好,都开了会有安全上的问题;
    1.4 WebSettings 的设置内容很多,如果想看更多的话可以进行搜索;
    1.5 暂未发现其他问题,待定;
  2. setWebChromeClient 和 setWebViewClient:
    2.1 这2个都是 webView 的配置属性,不过在功能上有所区分:
    WebViewClient帮助WebView处理各种通知、请求事件的
    WebChromeClient是辅助WebView处理Javascript的对话框,网站图标,网站title,加载进度等;
    js 里面使用 alert 和 confirm 需要在WebChromeClient里面进行修改,提供对话框;
    2.2 关于onPageFinished:
    如果你的路由里面是异步加载的,如resolve => require([‘./routers/XXX‘], resolve),那么就要注意,在每进入异步加载的页面后,都会触发此函数,所以如果你需要在页面加载后只执行一次的代码的话,就放在 setWebChromeClient 的 onProgressChanged 里进行判断进度是否为100时再执行;
  3. webview.loadUrl():
    3.1 这里的加载地址可以有2种,1是 webview.loadUrl(" file:///android_asset/index.html" ); 访问本地文件,2是webview.loadUrl(" http://www.baidu.com" ); 访问网络文件;
    各有其优点:若访问网络文件,更新服务器内容即可使用最新的功能; 而访问本地资源的话,加载的速度会快一点,而且即使断网也可以看到默认的东西;
刚刚有说到,进入 APP 的快慢问题,这里我是调用了一个加载的动画来完成的:
我这边选择的动画时这个:点击查看
而在 Android studio 里调用插件的方式十分简单:
  1. 打开根目录下的 build.gradle,在 allprojects 的 repositories 里添加:
    maven { url " https://jitpack.io" }

  2. 然后打开 app/build.gradle,在 dependencies 里添加:compile ‘com.github.zzz40500:android-shapeLoadingView:1.0.3.2‘
  3. 这时候先 build 项目,再在 src/main/res/layout/activity_main.xml 里添加代码:
    < android.support.constraint.ConstraintLayout > < com.mingle.widget.LoadingView android:id=" @+id/loadView" android:layout_width=" fill_parent" android:layout_height=" fill_parent" /> ...< /android.support.constraint.ConstraintLayout>

    这时候可以,这样 loading 动画就添加好了,后面只需要在 Java 代码里显示和隐藏就行了;
最关键的html:input[type=" file" ]问题,这个问题才是最大的问题,先说好
如果你的webApp不需要上传文件或者不在意Android 4.2-4.4 版本的话,可以用该方法
MainActivity.java:
先创建变量:
public static final int INPUT_FILE_REQUEST_CODE = 1; private ValueCallback< Uri> mUploadMessage; private final static int FILECHOOSER_RESULTCODE = 2; private ValueCallback< Uri[]> mFilePathCallback; private String mCameraPhotoPath;

setWebChromeClient里添加代码:
public void openFileChooser(ValueCallback uploadMsg, String acceptType) { Log.d(" 选择" , " 3.0+" ); mUploadMessage = uploadMsg; Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); i.setType(" image/*" ); MainActivity.this.startActivityForResult( Intent.createChooser(i, " Image Chooser" ), FILECHOOSER_RESULTCODE); }//Android 5.0 public boolean onShowFileChooser( WebView webView, ValueCallback< Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) { Log.d(" 选择" , " 5.0+" ); if (mFilePathCallback != null) { mFilePathCallback.onReceiveValue(null); }mFilePathCallback = filePathCallback; Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); if (takePictureIntent.resolveActivity(getPackageManager()) != null) { // Create the File where the photo should go File photoFile = null; try { //设置MediaStore.EXTRA_OUTPUT路径,相机拍照写入的全路径 photoFile = createImageFile(); takePictureIntent.putExtra(" PhotoPath" , mCameraPhotoPath); } catch (Exception ex) { // Error occurred while creating the File Log.e(" WebViewSetting" , " Unable to create Image File" , ex); }// Continue only if the File was successfully created if (photoFile != null) { mCameraPhotoPath = " file:" + photoFile.getAbsolutePath(); takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile)); System.out.println(mCameraPhotoPath); } else { takePictureIntent = null; } }Intent contentSelectionIntent = new Intent(Intent.ACTION_GET_CONTENT); contentSelectionIntent.addCategory(Intent.CATEGORY_OPENABLE); contentSelectionIntent.setType(" image/*" ); Intent[] intentArray; if (takePictureIntent != null) { intentArray = new Intent[]{takePictureIntent}; System.out.println(takePictureIntent); } else { intentArray = new Intent[0]; }Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER); chooserIntent.putExtra(Intent.EXTRA_INTENT, contentSelectionIntent); chooserIntent.putExtra(Intent.EXTRA_TITLE, " Image Chooser" ); chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, intentArray); startActivityForResult(chooserIntent, INPUT_FILE_REQUEST_CODE); return true; } // For Android 3.0+ public void openFileChooser(ValueCallback< Uri> uploadMsg) { Log.d(" 选择" , " 3.0+" ); mUploadMessage = uploadMsg; Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); i.setType(" image/*" ); MainActivity.this.startActivityForResult(Intent.createChooser(i, " Image Chooser" ), FILECHOOSER_RESULTCODE); }//For Android 4.1 public void openFileChooser(ValueCallback< Uri> uploadMsg, String acceptType, String capture) { Log.d(" 选择" , " 4+" ); mUploadMessage = uploadMsg; Intent i = new Intent(Intent.ACTION_GET_CONTENT); i.addCategory(Intent.CATEGORY_OPENABLE); i.setType(" image/*" ); MainActivity.this.startActivityForResult(Intent.createChooser(i, " Image Chooser" ), MainActivity.FILECHOOSER_RESULTCODE); }

在主类中添加:
@SuppressLint(" SdCardPath" ) private File createImageFile() { File file=new File(Environment.getExternalStorageDirectory()+" /" ," tmp.png" ); mCameraPhotoPath=file.getAbsolutePath(); if(!file.exists()) { try { file.createNewFile(); } catch (IOException e) { e.printStackTrace(); } } return file; } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(" result" , " show" ); if (requestCode == FILECHOOSER_RESULTCODE) { if (null == mUploadMessage) return; Uri result = data =https://www.songbingjia.com/android/= null || resultCode != RESULT_OK ? null : data.getData(); if (result != null) { String imagePath = ImageFilePath.getPath(this, result); if (!TextUtils.isEmpty(imagePath)) { result = Uri.parse(" file:///" + imagePath); } } mUploadMessage.onReceiveValue(result); mUploadMessage = null; } else if (requestCode == INPUT_FILE_REQUEST_CODE & & mFilePathCallback != null) { // 5.0的回调 Uri[] results = null; // Check that the response is a good one if (resultCode == Activity.RESULT_OK) { if (data == null & & !TextUtils.isEmpty(data.getDataString())) { // If there is not data, then we may have taken a photo if (mCameraPhotoPath != null) { results = new Uri[]{Uri.parse(mCameraPhotoPath)}; } } else { String dataString = data.getDataString(); if (dataString != null) { results = new Uri[]{Uri.parse(dataString)}; } } }mFilePathCallback.onReceiveValue(results); mFilePathCallback = null; } else { super.onActivityResult(requestCode, resultCode, data); return; } }

这里还需要一个 ImageFilePath 类文件,我将他放在 GitHub 里面了,后面我会附上链接:
解决方法来源:点击查看
至于 Android 4.2-4.4 会有问题是因为这个:点击查看
ps:需要FQ
而如果你是 native 开发者的话也比较容易解决,就是在点击时直接用 js 调用 Java 就行了,如果不是的话,一般都需要其他框架或者插件的支持;
我所碰到的问题基本就是这些,如果有错误和疏漏之处还请指出,谢谢;
GitHub:点击查看
【Android webView包装WebAPP】完;

    推荐阅读