前言 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 == 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:需要翻墙

而如果你是 native 开发者的话也比较容易解决,就是在点击时直接用 js 调用 Java 就行了,如果不是的话,一般都需要其他框架或者插件的支持;

我所碰到的问题基本就是这些,如果有错误和疏漏之处还请指出,谢谢;

GitHub:点击查看

完;

Android webView包装WebAPP的更多相关文章

  1. C#开发移动应用系列(2.使用WebView搭建WebApp应用)

    前言 上篇文章地址:C#开发移动应用系列(1.环境搭建) 嗯..一周了 本来打算2天一更的 - - ,结果 出差了..请各位原谅.. 今天我们来讲一下使用WebView搭建WebApp应用. 说明一下 ...

  2. webview之如何设计一个优雅健壮的Android WebView?(下)(转)

    转载:https://iluhcm.com/2018/02/27/design-an-elegant-and-powerful-android-webview-part-two/ (这篇文章写得有点晚 ...

  3. webview之如何设计一个优雅健壮的Android WebView?(上)(转)

    转接:https://iluhcm.com/2017/12/10/design-an-elegant-and-powerful-android-webview-part-one/ 前言 Android ...

  4. android WebView详解,常见漏洞详解和安全源码

    这篇博客主要来介绍 WebView 的相关使用方法,常见的几个漏洞,开发中可能遇到的坑和最后解决相应漏洞的源码,以及针对该源码的解析.  转载请注明出处:http://blog.csdn.net/se ...

  5. 如何设计一个优雅健壮的Android WebView?(下)

    转:如何设计一个优雅健壮的Android WebView?(下) 前言 在上文<如何设计一个优雅健壮的Android WebView?(上)>中,笔者分析了国内WebView的现状,以及在 ...

  6. 如何设计一个优雅健壮的Android WebView?(上)

    转:如何设计一个优雅健壮的Android WebView?(上) 前言 Android应用层的开发有几大模块,其中WebView是最重要的模块之一.网上能够搜索到的WebView资料可谓寥寥,Gith ...

  7. C# .Net 多进程同步 通信 共享内存 内存映射文件 Memory Mapped 转 VC中进程与进程之间共享内存 .net环境下跨进程、高频率读写数据 使用C#开发Android应用之WebApp 分布式事务之消息补偿解决方案

    C# .Net 多进程同步 通信 共享内存 内存映射文件 Memory Mapped 转 节点通信存在两种模型:共享内存(Shared memory)和消息传递(Messages passing). ...

  8. Android WebView与H5联调技巧

    版权声明:本文为xing_star原创文章,转载请注明出处! 本文同步自http://javaexception.com/archives/78 背景: 突然想写一篇关于Android WebView ...

  9. Android WebView 302斗争之旅

    一.背景 越来越多的业务接入,项目内多多少少会出现几个H5页面,只是单纯的提供WebView容器接入H5页面根本满足不了需求,他们需要登录态,需要制定协议控制Native的导航栏,或者需要JsBrid ...

随机推荐

  1. \(?0\d{2}[) -]?\d{8}正则表达式的解释

    正则表达式30分钟出现了这样一个正则表达式:(?0\d{2}[) -]?\d{8} "("和")"也是元字符,所以需要转义. ?匹配零或一次 (? 表示 出现( ...

  2. 从蓝光到4K,腾讯视频高码率下载背后的技术

    欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~ 由 腾讯技术工程官方号 发布在云+社区 蓝光和4k视频正逐渐普及,4K视频峰值码率超10Mbit/s.架构平台部TVideo平台从资源,链路.缓 ...

  3. Vivado常见问题集锦

    5. Vivado软件更新新版后更新IP 当更新到新版本的Vivado后,之前的一些工程的IP是不能直接打开使用的,这个时候我们只需要使用新版本的Vivado更新一下每个工程的IP即可,使用新版本Vi ...

  4. [linux] C语言Linux系统编程-TCP通信的11种状态

    三次握手由client主动发出SYN请求, 此时client处于SYN_SENT状态(第一次握手)当server收到之后会由LISTEN转变为SYN_REVD状态, 并回复client, client ...

  5. 分享一个JS的Base64加密解密功能

    代码实现: var base64 = (function () { var encodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop ...

  6. 【Java学习笔记之六】java三种循环(for,while,do......while)的使用方法及区别

    第一种:for循环 循环结构for语句的格式:       for(初始化表达式;条件表达式;循环后的操作表达式) { 循环体;    } eg: class Dome_For2{ public st ...

  7. 4395: [Usaco2015 dec]Switching on the Lights

    每次到达一个点,或者点亮一个房间的灯的时候,检查一下它四周的点能否走. 一开始看错题了..要求的是最多能开多少房的灯. #include<cstdio> #include<iostr ...

  8. c语言基础学习01

    =============================================================================每一种语言都有其独特的语法规则与语言定义. 指 ...

  9. jquery实现上下滑动选择

    $('.rightShow').on('mousewheel', function(ev) { var dir = ev.originalEvent.wheelDelta if(dir == 120) ...

  10. ABB安全区域(全局区域)的指令解析

    VAR wztemporary wzone;//VAR:变量 //wztemporary:全局区域数据类型(wztemporary临时全局区域数据.wzstationary固定式全局区域)wzone: ...