灵活高效的在 Android Native App 开发中显示 HTML 内容

通过 WebView 或者 TextView 显示 HTML 内容的最佳实践和对比

在 Android Native App 的开发过程中,经常会遇到的一种场景就是要显示 HTML 内容,而 Android SDK 本身也提供了多种方式供开发者来呈现 HTML 内容。

张 谦, 软件工程师, IBM

张谦,2009 年加入 IBM,目前就职于 IBM CDL 从事 Cognos ReportServe r的开发工作,并有多年的 Android 开发经验。



陈 聪, 实习生, IBM

陈聪,IBM CDL实习生,从事 Android Native App 开发工作。



李 伟, 实习生, IBM

李伟,IBM CDL实习生,从事 Android Native App 开发工作。



2014 年 7 月 10 日

其中使用最多的两种方式分别为:

  • 使用 Android 提供的 WebView 控件。
  • 通过将 HTML 内容转化为 Spanned 格式在 TextView 中进行显示。

虽然这两种方式都可以显示 HTML 内容,但是其对用户交互的响应方式却有较大的不同。本文通过两个实例来说明:1) 这两种方式的基本使用方式。2) 这两种方式的交互如何实现。3) 通过对比阐述这两种方式各适合于哪些应用场景。

背景

对于有显示 HTML 内容的 Android 应用来说,使用 Android SDK WebView 来显示是最简单的方式,但是并不是所有的场景下都适合使用 WebView 来显示 HTML 内容,例如,如果应用要显示的内容只是一部分 HTML 片段,就可以利用 TextView 来进行显示,并且效率较高。

另外,使用 WebView 或者 TextView 来显示 HTML 内容,其交互的实现方式有较大的区别,以在 HTML 内容中的图片点击事件为例,在 WebView 模式下,开发人员需要通过注入 JavaScript 代码来进行点击事件的响应,Android 系统提供了 WebView 中 JavaScript 网页脚本调用 Java 类方法的机制;而在 TextView 中,图片会被解析为 ImageSpan,通过在 ImageSpan 上注册点击事件来响应。 本文后两节分别通过实例来讲述这两种方式的使用方法,以及其用户交互方式。


通过 WebView 显示 HTML 内容及用户交互

Android 中的 WebView 组件经常用来加载类似 HTML 这样的格式化文本,它强大的注入功能能轻易实现 JavaScript 代码与 Java 代码之间的交互。本文通过一个详细的例子一步一步教你学会使用并理解 WebView。

在 WebView 中显示 HTML 内容

Android 的 WebView 组件使用非常简单,可以使用 loadUrl()加载一个 URL 地址,也可以使用 loadData()或 loadDataWithBaseURL()加载一段 HTML 代码片段。loadData()不能加载图片内容,所以在本例中选择使用可以加载图片内容,并获得更强大 Web 支持的 loadDataWithBaseURL()来显示文字与图片内容,如清单 1中所示。

清单 1. loadDataWithBaseURL()方法定义

点击查看代码清单

清单 1. loadDataWithBaseURL()方法定义

public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl) {
  checkThread();
  if (DebugFlags.TRACE_API) Log.d(LOGTAG, "loadDataWithBaseURL=" + baseUrl);
 mProvider.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
  }
  • baseUrl,相对路径,在本例中 HTML 文本内用到的所有资源文件,不论是图片还是其它的 JavaScript 文件或者 CSS 文件,其路径都是相对于这个参数的。
  • data, 以 encoding 格式编码的 HTML 内容字符串。
  • mimeType,数据的 MIME 类型,null 时默认为"text/html"。
  • encoding, 数据编码格式。
  • historyUrl,作为历史条目的 URL,可为 null。

用户交互事件的添加

对 loadDataWithBaseURL()方法传入参数前,对参数 data 字符串做处理:

String data = webContentHandle(DataUtil.getFromAssets(this, "htmlsample/sample.html"));
webview.loadDataWithBaseURL(BASE_PATH + "htmlsample/", data,"text/html", "utf-8", null);

在 webContentHandle()方法中为 HTML 内容增加 JavaScript 代码以实现两张图片分别添加 HTML 的 onclick 事件,并使图片能够全屏显示,还支持按键退出全屏显示。

本例在实现图片点击后全屏显示时,使用了两种方法,分别作用于两张图片。

第一张图片响应点击,会创建新的 Activity 来全屏显示图片(跳出 WebView,JavaScript 调用 Java),按退出键则退出全屏显示(Java 方法)。

第二张图片响应点击,利用纯 HTML 技术实现全屏显示(未跳出 WebView),按退出键退出全屏显示(Java 调用 JavaScript)。

JavaScript 调用 Java

清单 2. JavaScriptInterface 类定义
public class JavaScriptInterface {
	......
	@JavascriptInterface
	public void closeApp() {
	activity.finish();
	}	
	@JavascriptInterface
	public void showImage(final String imagePath) {
	Intent i = new Intent(activity, ImageActivity.class);
	Bundle bundle = new Bundle();
	bundle.putString(IMAGE_PATH_KEY, imagePath);
	i.putExtras(bundle);
	activity.startActivity(i);
	}
}

所有被 JavaScript 代码调用的 Java 方法都必须被预定义在一个类里如清单 2 所示,本例有两个被调用的方法即 closeApp()和 showImage(),被调用的方法可以带参也可以不带参,值得注意的是参数类型单一。

webview.getSettings().setJavaScriptEnabled(true);
webview.addJavascriptInterface(new JavaScriptInterface(this),"JSInterface");

要实现 WebView 与 JavaScript 的交互必须先打开 WebView 对 JavaScript 的支持功能。JavaScript 调用 Java 是通过 addJavascriptInterface()方法来实现的,该方法的第一个参数是一个包含被 JavaScript 代码调用的 Java 方法的对象,本例中就是类 JavaScriptInterface 的实例,第二个参数是该对象在 JavaScript 代码中的变量名,注意该变量是位于全局即 window 下的,即可以在 JavaScript 代码中用"window.变量名.方法名"来调用第一个参数中的方法。对于不带参的 Java 方法调用很简单如 closeApp(),但是对于带参的方法如上面的 showImage(),要注意的是参数的拼接,易拼错,且不易检查,详见代码中的 webContentHandle()方法。

Java 调用 JavaScript

清单 3. 对按返回键的处理
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
	webview.loadUrl("javascript: window.backAction();");
	return true;
}
return super.onKeyDown(keyCode, event);
}

Java 调用 JavaScript 用的是 WebView 的 loadUrl()方法,该方法不仅能以最常见的 HTTP 地址作为参数,还可以是 JavaScript 代码。清单 3中方法使得用户一旦按下退出键,就会通知 WebView 容器去触发 JavaScript 代码 window.backAction()的执行。

原始 HTML 文本仅仅是文字加两张图片,没有任何点击事件,经过增加 JavaScript 交互处理后图片有了点击事件,并且还能响应设备上的退出键。

在本例中第一张图片用户交互式体验效果如下图 1

在本例中第二张图片用户交互式体验效果如下图 2

图 1
图 1
图 2
图 2

通过 TextView 显示 HTML 内容及用户交互

Android 中的 TextView 组件常用于显示文本内容,其实它也可以显示 HTML 的内容。简单来讲,这就需要先把 HTML 的内容以字符串的形式获取后,经过 android.text.Html.fromHtml()转化成 Spanned 的格式,然后将其传递到 TextView 的 setText()方法中,这样就可以在 TextView 中显示 HTML 页面的内容了。需要注意的是,并不是所有的 HTML 标签在 TextView 中都是支持的,且官方文档并没有明确的说明支持 HTML 标签列表,通过查看 Android 源代码,可以得到简单的支持列表。

点击查看代码清单

{<br>,< p>,< div align=>,< strong>, <b>, <em>, <cite>, <dfn>, <i>, <big>, <small>, <font size=>,  <font color=>, <blockquote>, <tt>, <a href=>, <u>, <sup>, <sub>, <h1>,<h2>,<h3>,<h4>,<h5>,<h6>, <img src=>, <strike>}

下面的示例来介绍如何在 TextView 中显示一段 HTML 内容,要显示的这段 HTML 内容即包含超链接内容,也包含有图片,示例中也会演示如何在图片上注册点击事件来完成用户交互,工程代码可以再附件中直接下载(附链接)。

在 TextView 中显示 HTML 内容

显示的过程中最主要的过程就是调用 Android.text.Html 类提供的 fromHtml()方法,将一段 HTML 内容转化为 Spanned 对象。

Android.text.Html 类提供的 fromHtml()方法使用如下清单 4

清单 4. fromHtml()方法定义
public static Spanned fromHtml(String source, ImageGetter imageGetter,
	TagHandler tagHandler) {
        ……
HtmlToSpannedConverter converter =
new HtmlToSpannedConverter(source, imageGetter, tagHandler,  parser);
return converter.convert();
}
  • source,就是包含 HTML 内容的字符串。而 Html.ImageGetter 和 Html.TagHandler 是两个接口,提供给开发者继承使用。
  • imageGetter, 如果要显示图片是需要被继承的,重写 getDrawable(String source)方法,用于获取 HTML 里面的图片来显示在 TextView 中。
  • tagHandler,其作用是把 HTML 带标记的文本内容字符串转化成可以显示效果的的 Spanned 字符串 。由于并非所有的 HTML 标签都可以转化,所以在使用时,用户需要自己添加一些必要的标签和处理方法时才会继承使用的。

    在本例中使用 fromHtml()方法之前,要准备好该方法要用的三个参数内容,首先将 HTML 字符串内容准备好,在项目中需要创建两个类 MImageGetter 和 MTagHandler 分别继承于 ImageGetter 和 TagHandler,分别用户图片的获取,和特殊标签的支持。

  • MImageGetter

    继承于 ImageGetter,重写 getDrawable (String source) 方法中从 assets 路径下取出的图片流(这里当然也可以通过网络操作来完成图片流的获取),最后获得可供显示的图片对象,例如 Drawable 对像。由于 Android 设备的异构性,为了有更好的显示效果,通常需要获取屏幕大小,然后调用 drawable.setBounds () 还可以重新设置图片的大小, 最后返回合适大小的图片 Drawable 对象。 由此 Spanned 中的 ImageSpan 就获得了图像被显示在 TextView 中对应位置了。

    点击查看代码清单

    TypedValue typedValue = new TypedValue();
    typedValue.density = TypedValue.DENSITY_DEFAULT;
    drawable = Drawable.createFromResourceStream(null, typedValue, is, "src");
    DisplayMetrics dm = c.getResources().getDisplayMetrics();  
    int dwidth = dm.widthPixels-10;//padding left + padding right
    float dheight = (float)drawable.getIntrinsicHeight()*(float)dwidth/(float)drawable.getIntrinsicWidth();
    int dh = (int)(dheight+0.5);
    int wid = dwidth;
    int hei = dh;
    drawable.setBounds(0, 0, wid, hei);DisplayMetrics dm = c.getResources().getDisplayMetrics();
  • MTagHandler

    继承于 TagHandler,重写了 handleTag()方法,为的是支持<ul><ol><dd>和<li>标签,这四个标签是在 formHtml()方法中本身是不支持。如果开发者认为安卓 TagHandler 提供的默认标签解析已经够用,直接在 fromHtml()方法中第三个参数的地方填写 null 既可。

    清单 5. 重写 handleTag()方法

    点击查看代码清单

    清单 5. 重写 handleTag()方法

    public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) {
    if (tag.equals("ul") || tag.equals("ol") || tag.equals("dd")) {
    	if (opening) {
    	mListParents.add(tag);
    	} else mListParents.remove(tag);
    } else if (tag.equals("li") && !opening) {
    	 handleListTag(output);
    }
    }
    private void handleListTag(Editable output) {
    ……
     }

    最后,在完成了 MImageGetter、MTagHandler 以后,就可以通过 formHtml()方法将 HTML 内容转化为可供显示的 SpannableString,将 SpannableString 通过 setText 方法放入 TextView 中,就可以显示图文并茂的内容了。

    progressBar.setVisibility(View.GONE);
    text.setText(Html.fromHtml(htmlCont, new MImageGetter(text,MainActivity.this), new MTagHandler()));
    text.setVisibility(View.VISIBLE);

    在本例中用 TextView 展现 HTML 内容效果如下3

    图 3
    图 3

用户交互事件的添加

在上一步骤,formHtml()方法已经将 HTML 内容中的超链接和图片转义成为 UrlSpan 和 ImageSpan,进而在 TextView 中完成显示。但是此时是没有任何用户交互的,用户只能看到 HTML 的内容,下面介绍如何添加用户交互功能。

要完成用户交互,这里我们需要在 TextView 中还需要调用 textView.setMovementMethod()方法,Android 提供了 LinkMovementMethod 类以实现了对于文本内容中超链接的遍历,并且支持对于超链接的点击事件。所以只要在添加一句 setMovementMethod(LinkMovementMethod.getInstance())就可以使点击 UrlSpan 能够触发打开链接的功能。

但是本例仅使用 setMovementMethod ()方法还不够,为了更好地用户体验,不仅 UrlSpan 能够被触发,ImageSpan 也要求可以跳转到只看到图片的全屏显示的功能。

特此创建一个 LinkMovementMethodExt 类继承于 LinkMovementMethod,在能够触发 UrlSpan 的基础上,也使 IamgeSpan 可以被触发。在 LinkMovementMethodExt 中重写方法 onTouchEvent, 将 ImageSpan 的类型传入,并使点击图片才能向 Handler 传递 Message,滑动图片并且不影响 TextView 内容的展示,这样就不会影响有图片文章的向下滑动阅读。

清单 6. 创建 LinkMovementMethodExt 类

点击查看代码清单

清单 6. 创建 LinkMovementMethodExt 类

public class LinkMovementMethodExt extends LinkMovementMethod {
……
@Override
 public boolean onTouchEvent(TextView widget, Spannable buffer,   MotionEvent event) {
……
	Object [] spans = buffer.getSpans(off, off, spanClass);
	if (spans.length != 0) {
	 if (action == MotionEvent.ACTION_DOWN) {
		Selection.setSelection(buffer, buffer.getSpanStart(spans[0]),  buffer.getSpanEnd(spans[0]));
		MessageSpan obj = new MessageSpan();
		obj.setObj(spans);
		obj.setView(widget);
		Message message = handler.obtainMessage();
		message.obj = obj;
		message.sendToTarget();
		 ……
	  }
	} ……

其中 Handler 用来处理从 HTML 中获得的 Span,把 LinkMovementMethodExt 中点击的内容传输到 MainActivity 之中,并对于 Span 类型做以判断,如果是 ImageSpan 就可以打开一个 ShowPicActivity,将图片以 ImageView 显示在其中。最终产生的效果为:可以在点击超链接时请求打开浏览器看超链接的内容,同时也可以在 TextView 显示的 HTML 内容中点击图片就可以跳转到 ShowPicActivity,先调用 reSizePicture()显示合适横屏或纵屏全屏大小的图片,将图片以 ImageView 的格式展示。

清单 7.点击 ImageSpan 后的处理
if (span instanceof ImageSpan) {
Intent intent = new Intent(c, ShowPicActivity.class);
        Bundle bundle = new Bundle();
bundle.putString("picUrl",((ImageSpan) span).getSource());
intent.putExtras(bundle);      		
startActivity(intent);
}

由于图片被以 ImageView 的格式展示出来,可以对于图片做出各种处理,比如使用对图片增加手势缩放,旋转的,这些就是对于 ImageView 的操作,在这里就不一一赘述了。不过这些效果都会提供有很好的用户体验。

在本例中用户交互式体验效果如下4

图 4
图 4

总结

通过以上两部分的描述以及示例代码,相信读者已经对在 Android Native App 中如果显示 HTML 内容有了基本的了解,这里笔者通过在自己实际项目中的经验对这两种方法进行比较。在笔者所参与的项目中,也有显示 HTML 页面内容的需求,经过实际比较,

  • 如果 HTML 内容比较复杂,还是建议使用 WebView 作为显示方式,1. TextView 里面不是所有的 HTML 标签都支持,需要开发者额外增加对于标签的支持,2. Android 也提供了 WebView 和 App 之间的交互方式,使用它也可以满足基本需求。
  • 而对于显示格式化文本这样的需求,使用 TextView 就再合适不过了,可以再服务器端进行格式化文本的按照 HTML 格式的组装,在应用端直接显示,这样显示的效果在服务端完全控制,且相比使用 WebView 更轻量级。

下载

描述名字大小
示例代码Demo_projects.zip5258KB

参考资料

学习

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Web development
ArticleID=977372
ArticleTitle=灵活高效的在 Android Native App 开发中显示 HTML 内容
publish-date=07102014