Webview

目前很多android app都内置了可以显示web页面的界面,会发现这个界面一般都是由一个叫做WebView的组件渲染出来的,学习该组件可以为你的app开发提升扩展性。 

先说下WebView的一些优点: 

  • 可以直接显示和渲染web页面,直接显示网页 
  • webview可以直接用html文件(网络上或本地assets中)作布局 
  • 和JavaScript交互调用

一、基本使用 

首先layout中即为一个基本的简单控件: 

 
1
2
3
4
5
<WebView
        android:id="@+id/webView1"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:layout_marginTop="10dp" />

同时,因为要房访问网络,所以manifest中必须要加uses-permission: 

 
1
<uses-permission android:name="android.permission.INTERNET"/>

在activity中即可获得webview的引用,同时load一个网址: 

 
1
2
3
webview = (WebView) findViewById(R.id.webView1);
webview.loadUrl("http://www.baidu.com/");
//webview.reload();// reload page

这个时候发现一个问题,启动应用后,自动的打开了系统内置的浏览器,解决这个问题需要为webview设置 WebViewClient,并重写方法: 

 
1
2
3
4
5
6
7
webview.setWebViewClient(new WebViewClient(){
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return true;
            }
        });

若自己定义了一个页面加载进度的progressbar,需要展示给用户的时候,可以通过如下方式获取webview内页面的加载进度: 

 
1
2
3
4
5
6
webview.setWebChromeClient(new WebChromeClient(){
            @Override
            public void onProgressChanged(WebView view, int newProgress) {
                //get the newProgress and refresh progress bar
            }
        });

每个页面,都有一个标题,比如www.baidu.com这个页面的title即“百度一下,你就知道”,那么如何知道当前webview正在加载的页面的title呢: 

 
1
2
3
4
5
6
webview.setWebChromeClient(new WebChromeClient(){
            @Override
            public void onReceivedTitle(WebView view, String title) {
                titleview.setText(title);//a textview
            }
        });

二、通过webview控件下载文件 

通常webview渲染的界面中含有可以下载文件的链接,点击该链接后,应该开始执行下载的操作并保存文件到本地中。webview来下载页面中的文件通常有两种方式: 

1. 自己通过一个线程写java io的代码来下载和保存文件(可控性好) 

2. 调用系统download的模块(代码简单) 

方法一: 

首先要写一个下载并保存文件的线程类 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class HttpThread extends Thread {
  private String mUrl;
  public HttpThread(String mUrl) {
    this.mUrl = mUrl;
  }
  @Override
  public void run() {
    URL url;
    try {
      url = new URL(mUrl);
      HttpURLConnection conn = (HttpURLConnection) url.openConnection();
      conn.setDoInput(true);
      conn.setDoOutput(true);
      InputStream in = conn.getInputStream();
      File downloadFile;
      File sdFile;
      FileOutputStream out = null;
      if(Environment.getExternalStorageState().equals(Environment.MEDIA_UNMOUNTED)){
        downloadFile = Environment.getExternalStorageDirectory();
        sdFile = new File(downloadFile, "test.file");
        out = new FileOutputStream(sdFile);
      }
      //buffer 4k
      byte[] buffer = new byte[1024 4];
      int len = 0;
      while((len = in.read(buffer)) != -1){
        if(out != null)
          out.write(buffer, 0, len);
      }
      //close resource
      if(out != null)
        out.close();
      if(in != null){
        in.close();
      }
    catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
}

随后要实现一个DownloadListener接口,这个接口实现方法OnDownloadStart(),当用户点击一个可以下载的链接时,该回调方法被调用同时传进来该链接的URL,随后即可以对该URL塞入HttpThread进行下载操作: 

 
1
2
3
4
5
6
7
8
9
10
11
//创建DownloadListener (webkit包)
class MyDownloadListenter implements DownloadListener{
  @Override
  public void onDownloadStart(String url, String userAgent,
    String contentDisposition, String mimetype, long contentLength) {
      System.out.println("url ==== >" + url);
      new HttpThread(url).start();
  }
    }
//给webview加入监听
webview.setDownloadListener(new MyDownloadListenter());

方法二: 

直接发送一个action_view的intent即可: 

 
1
2
3
4
5
6
7
8
9
10
11
12
class MyDownloadListenter implements DownloadListener{
        @Override
        public void onDownloadStart(String url, String userAgent,
      String contentDisposition, String mimetype, long contentLength) {
  System.out.println("url ==== >" + url);
  //new HttpThread(url).start();
   
  Uri uri = Uri.parse(url);
  Intent intent = new Intent(Intent.ACTION_VIEW, uri);
  startActivity(intent);
        }
    }

三、错误处理 

当我们使用浏览器的时候,通常因为加载的页面的服务器的各种原因导致各种出错的情况,最平常的比如404错误,通常情况下浏览器会提示一个错误提示页面。事实上这个错误提示页面是浏览器在加载了本地的一个页面,用来提示用户目前已经出错了。 

但是当我们的app里面使用webview控件的时候遇到了诸如404这类的错误的时候,若也显示浏览器里面的那种错误提示页面就显得很丑陋了,那么这个时候我们的app就需要加载一个本地的错误提示页面,即webview如何加载一个本地的页面。 

1. 首先我们需要些一个html文件,比如error_handle.html,这个文件里面就是当出错的时候需要展示给用户看的一个错误提示页面,尽量做的精美一些。然后将该文件放置到代码根目录的assets文件夹下。 

2. 随后我们需要复写WebViewClient的onRecievedError方法,该方法传回了错误码,根据错误类型可以进行不同的错误分类处理 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
webview.setWebViewClient(new WebViewClient(){
        
      @Override
      public void onReceivedError(WebView view, int errorCode,
        String description, String failingUrl) {
    switch(errorCode)
    {
    case HttpStatus.SC_NOT_FOUND:
        view.loadUrl("file:///android_assets/error_handle.html");
        break;
    }
      }
  });

其实,当出错的时候,我们可以选择隐藏掉webview,而显示native的错误处理控件,这个时候只需要在onReceivedError里面显示出错误处理的native控件同时隐藏掉webview即可。 

四、webview同步cookies 

cookies是服务器用来保存每个客户的常用信息的,下次客户进入一个诸如登陆的页面时服务器会检测cookie信息,如果通过则直接进入登陆后的页面。 

在webview中,如果之前已经登陆过了,那么下次再进入同样的登陆界面时,若需要再次登陆的话,一定会很恼人,所以这里提供一个webview同步cookies的方法。 

1.首先,我们假设某个网站的登陆界面需要提供两个参数,一个是name,一个是pwd,那么要是对这个页面进行登陆,那么必须给与这两个信息。我们假设服务器已经注册了name为jason,pwd为123456这个账号。 

2.下面,写一个Thread用来将name和pwd自动的登入,在服务器返回的response中获得cookie信息,稍后对这个cookie进行保存,这里先给出这个Thread的代码: 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class HttpCookie extends Thread {
  private Handler mHandler;
  public HttpCookie(Handler mHandler) {
    this.mHandler = mHandler;
  }
  @Override
  public void run() {
    HttpClient client = new DefaultHttpClient();
    HttpPost post = new HttpPost("");//this place should add the login address
     
    List<NameValuePair> list = new ArrayList<NameValuePair>();
    list.add(new BasicNameValuePair("name""jason"));
    list.add(new BasicNameValuePair("pwd""123456"));
    try {
      post.setEntity(new UrlEncodedFormEntity(list));
      HttpResponse reponse = client.execute(post);
      if(reponse.getStatusLine().getStatusCode() == HttpStatus.SC_OK){
        AbstractHttpClient absClient = (AbstractHttpClient) client;
        List<Cookie> cookies = absClient.getCookieStore().getCookies();
        for(Cookie cookie:cookies){
          if(cookie != null){
            //TODO
            //this place would get the cookies
          }
        }
      }
    catch (UnsupportedEncodingException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    catch (ClientProtocolException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    catch (IOException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
}

由于这是一个子线程,所以需要在主线程中创建并执行。 

同时,因为其实子线程,那么里面必须含有一个handler的元素,用来当成功获取cookie后通知主线程进行同步和保存。初始化这个子线程的时候需要将主线程上的handler给传过来,随后在以上代码的TODO中发送消息,让主线程记录cookie,发送的这个消息需要将cookie信息包含进去: 

 
1
2
3
4
5
6
7
8
9
10
if(cookie != null){
  //TODO
  //this place would get the cookies
  Message msg = new Message();
  msg.obj = cookie;
  if(mHandler != null){
    mHandler.sendMessage(msg);
    return;
  }
}

随后在主线程中(webview加载登陆界面前),在handler中将会获取到cookie信息,下面将对该cookie进行保存和同步: 

 
1
2
3
4
5
6
7
8
9
10
11
private Handler mHandler = new Handler(){
    public void handleMessage(android.os.Message msg) 
    {
      CookieSyncManager.createInstance(MainActivity.this);
      CookieManager cookieMgr = CookieManager.getInstance();
      cookieMgr.setAcceptCookie(true);
      cookieMgr.setCookie("", msg.obj.toString());// this place should add the login host address(not the login index address)
      CookieSyncManager.getInstance().sync();
      webview.loadUrl("");// login index address
    };
  };

这个时候发现webview加载的login index页面中可以自动的登陆了并显示登陆后的界面。 

五、 WebView与JavaScript的交互 

1. webview调用js 

 
1
mWebView.loadUrl("javascript:do()");

以上是webview在调用js中的一个叫做do的方法,该js所在的html文件大致如下: 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
  <script language="javascript">
    /* This function is invoked by the webview*/
    function do() {
      alert("1");
    }
  </script>
  <body>
    <a><div style="width:80px;
      margin:0px auto;
      padding:10px;
      text-align:center;
      border:2px solid #111111;" >
        <img id="droid" src="xx.png"/><br>
        Click me!
    </div></a>
  </body>
</html>

2. js调用webview 

我们假设下列的本地类是要给js调用的: 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.test.webview;
class DemoJavaScriptInterface {
  DemoJavaScriptInterface() {
  }
  /**
   * This is not called on the UI thread. Post a runnable to invoke
   * loadUrl on the UI thread.
   */
  public void clickOnAndroid() {
      mHandler.post(new Runnable() {
    public void run() {
        //TODO
    }
      });
  }
    }

首先给webview设置: 

 
1
mWebview.setJavaScriptEnabled(true);

随后将本地的类(被js调用的)映射出去: 

 
1
mWebView.addJavascriptInterface(new DemoJavaScriptInterface(), "demo");

“demo”这个名字就是公布出去给JS调用的,那么js久可以直接用下列代码调用本地的DemoJavaScriptInterface类中的方法了: 

 
1
2
3
<body>  
    ...
</body>

六、WebView与JavaScript相互调用混淆问题 

若webview中的js调用了本地的方法,正常情况下发布的debug包js调用的时候是没有问题的,但是通常发布商业版本的apk都是要经过混淆的步骤,这个时候会发现之前调用正常的js却无法正常调用本地方法了。 

这是因为混淆的时候已经把本地的代码的引用给打乱了,导致js中的代码找不到本地的方法的地址。 

解决这个问题很简单,即在proguard.cfg文件中加上一些代码,声明本地中被js调用的代码不被混淆。下面举例说明: 

第五节中被js调用的那个类DemoJavaScriptInterface的包名为com.test.webview,那么就要在proguard.cfg文件中加入: 

 
1
2
3
-keep public class com.test.webview.DemoJavaScriptInterface{
    public <methods>;
}

若是内部类,则大致写成如下形式: 

 
1
2
3
-keep public class com.test.webview.DemoJavaScriptInterface$InnerClass{
    public <methods>;
}

若android版本比较新,可能还需要添加上下列代码: 

 
1
2
-keepattributes *Annotation* 
-keepattributes *JavascriptInterface*

WebView是安卓中用来显示html文本内容的的控件,对html5也有很好的支持,ios的控件UIWebView差不多。网上对WebView的解释很多,但都是零星的介绍,导致到现在为止webview给我的印象都是,貌似很强大,其实很鸡肋,于是决定总结一下webview的开发经验。

使用WebView并不需要开通网络权限

网上有文章说webview需要开通internet权限,否则会出Web page not available错误,这是不对的,出现Web page not available并不是因为使用了webview,而是webview访问了网络,如果webview只是加载本地html(比如assets目录中的文件),或者只是加载带有html文本的字符串,即使没有internet权限,也不会报错。

如何调用webview

xml中

1
2
3
4
5
<WebView
    android:id="@+id/blog_detail_webview"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:background="#FFFFFF"/>

activity中

1
2
3
4
5
6
mWebView = (WebView)findViewById(R.id.blog_detail_webview);
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.getSettings().setSupportZoom(false);
mWebView.getSettings().setBuiltInZoomControls(false);
mWebView.getSettings().setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN);
mWebView.getSettings().setDefaultFontSize(18);

基本设置

上面的java代码部分相信大家都懂,可以看到WebView 和其他控件不同的地方在于其属性设置是调用mWebView.getSettings()来完成的,不知道谷歌这样设计的用意,其中:

mWebView.getSettings().setJavaScriptEnabled(false);

表示不支持js,如果想让java和js交互或者本身希望js完成一定的功能请把false改为true。

mWebView.getSettings().setSupportZoom(false);

设置是否支持缩放,我这里为false,默认为true。

mWebView.getSettings().setBuiltInZoomControls(false);

设置是否显示缩放工具,默认为false。

mWebView.getSettings().setLayoutAlgorithm(LayoutAlgorithm.SINGLE_COLUMN);

一般很少会用到这个,用WebView组件显示普通网页时一般会出现横向滚动条,这样会导致页面查看起来非常不方便。LayoutAlgorithm是一个枚举,用来控制html的布局,总共有三种类型:
NORMAL:正常显示,没有渲染变化。
SINGLE_COLUMN:把所有内容放到WebView组件等宽的一列中。
NARROW_COLUMNS:可能的话,使所有列的宽度不超过屏幕宽度。

mWebView.getSettings().setDefaultFontSize(18);

设置默认的字体大小,默认为16,有效值区间在1-72之间。

加载内容

(1)加载assets目录下的本地网页

一般我们都是把html文件放在assets目录下, WebView调用assets目录下的本地网页和图片等资源非常方便,使用形如

1

的调用方法即可。

(2)加载远程网页

1
mWebView.loadUrl("http://www.google.com");

(3)使用 LoadData 或者 loadDataWithBaseURL方法加载内容

有时候我们的webview可能只是html片段,而不是一个完整的网页,事实上绝大多数时候都是如此,完整的网页无需做成应用,而直接在浏览器访问。

这种情况我们使用 LoadData 或者 loadDataWithBaseURL方法,后者用的最多:

void loadDataWithBaseURL (String baseUrl, String data, String mimeType, String encoding, String historyUrl)

loadDataWithBaseURL()比loadData()多两个参数,可以指定HTML代码片段中相关资源的相对根路径,也可以指定历史Url,其余三个参数相同。

这里主要注意参数baseUrl,baseUrl指定了你的data参数中数据是以什么地址为基准的,因为data中的数据可能会有超链接或者是image元素,而很多网站的地址都是用的相对路径,如果没有baseUrl,webview将访问不到这些资源。

举个例子:

1
2
String body ="示例:这里有个img标签,地址是相对路径<img src='/uploads/allimg/130923/1FP02V7-0.png' />";
mWebView.loadDataWithBaseURL("http://www.jcodecraeer.com", body, "text/html", "utf-8",null);

如果baseUrl没有指定为http://www.jcodecraeer.com,那么这张图片将显示不出来。

上面的例子其实演示了loadDataWithBaseURL的用法,我们直接加载一个字符串里面的html内容,而有些时候这些内容是从assets目录下的本地网页文件中读取,下面我们将html/test1.html中的内容通过LoadData来加载:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
String data = "";
try {
    // 读取assets目录下的文件需要用到AssetManager对象的Open方法打开文件
    InputStream is = getAssets().open("html/test2.html");
    // loadData()方法需要的是一个字符串数据所以我们需要把文件转成字符串
    ByteArrayBuffer baf = new ByteArrayBuffer(500);
    int count = 0;
    while ((count = is.read()) != -1) {
        baf.append(count);
    }
    data = EncodingUtils.getString(baf.toByteArray(), "utf-8");
} catch (IOException e) {
    e.printStackTrace();
}
// 下面两种方法都可以加载成功
mWebView.loadData(data, "text/html", "utf-8");
// wv.loadDataWithBaseURL("", data, "text/html", "utf-8", "");

这种通过读取文件再用loadData加载其实和mWebView.loadUrl("file:///android_asset/html/test1.html")是一致的,只不过loadData方式因为没有指定地址的基准url,html/test1.html文件中一些资源文件或者链接地址会失效。

loadDataWithBaseURL和loadData两个方法加载的HTML代码片段的不同点在于,loadData()中的html data中不能包含'#', '%', '\', '?'四中特殊字符,在平时测试时,你的数据时,你的数据里含有这些字符,但不会出问题,当出问题时,你可以替换下。

      %,会报找不到页面错误,页面全是乱码。乱码样式见符件。

      #,会让你的goBack失效,但canGoBAck是可以使用的。于是就会产生返回按钮生效,但不能返回的情况。

WebView内容的处理

 

android 中webView控件 padding不起作用
在一个布局文件中有一个WebView,想使用padding属性让左右向内留出一些空白,但是padding属性不起左右,内容照样贴边显示,反而移动了右边滚动条的位置。android的bug,用一个外围的layout包含webview,可以有所改进,但不能完全解决。其实正确的做法是在webView的加载的css中增加padding,没必要为了padding而更改xml布局文件。


重写shouldOverrideUrlLoading时指定url

指定只有url里包含eoe.cn的时候才在webview里打开,否则还是启动浏览器打开.

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    LogUtil.i(this, "url="   url);
    if ( url.contains("eoe.cn") == true){
        view.loadUrl(url);
        return true;
    }else{
        Intent in = new Intent (Intent.ACTION_VIEW , Uri.parse(url));
        startActivity(in);
        return true;
    }
}


android:scrollbarStyle控制滚动条位置

WebView有一个设置滚动条位置的属性:android:scrollbarStyle 可以是insideOverlay可以是outsideOverlay,两个的区别是SCROLLBARS_INSIDE_OVERLAY的样式是滚动条在整个page里,类似css中的padding,看代码下的这个图吧,很清晰.

 

mWebView.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);

mWebView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);

Screen Shot 2013-02-23 at 9.30.00 PM.png

一般如果一个静态webview窗口想要加载css文件可以按如下方式处理:

String css = "<link rel=\"stylesheet\" href=\"file:///android_asset/css/news.css\" type=\"text/css\">";
                String html = "<html><head>" + css + "</head><body>" + content.getBody() + "</body></html>";
                html = html.replace("<div class=\"img-place-holder\">", "");
                mWebView.loadDataWithBaseURL("x-data://base", html, "text/html", "UTF-8", null);

一般css文件针对手机屏幕会处理一些适配问题,比如图片超过了屏幕大小等问题
下面的url是一些webview相关的资源网站:

http://blog.csdn.net/woshinia/article/details/14127025
posted @ 2015-05-15 15:36  CoolRandy  阅读(215)  评论(0)    收藏  举报