安卓TextView完美展示html格式代码

笛里谁知壮士心,沙头空照征人骨。这篇文章主要讲述安卓TextView完美展示html格式代码相关的知识,希望能为你提供帮助。
对于TextView展示html格式代码, 最简单的办法就是使用textview.setText(Html.fromHtml(html)); , 即便其中有img标签, 我们依然可以使用ImageGetter, 和TagHandler对其中的图片做处理, 但用过的都知道, 效果不太理想, 甚至无法满足产品简单的需求, 那么今天博主就来为大家提供一个完美的解决方案!
html代码示例:

安卓TextView完美展示html格式代码

文章图片

效果图:
安卓TextView完美展示html格式代码

文章图片

首先, 要介绍一个开源项目, 因为本篇博客所提供的方案是基于这个项目并进行扩展的:
https://github.com/NightWhistler/HtmlSpanner
该项目对html格式代码( 内部标签和样式) 基本提供了所有的转化方案, 效果还是蛮不错的, 但对于图片的处理仅做了展示, 而对大小设置, 点击事件等并未给出解决方案, 所以本篇博客即是来对其进行扩展完善, 满足日常开发需求!
首先, 看HtmlSpanner的使用方法( 注: HtmlSpanner内部代码实现不做详细分析, 有兴趣的可下载项目研究) :
textView.setText(htmlSpanner.fromHtml(html));

htmlSpanner.fromHtml(html)返回的是Spannable格式数据, 使用非常简单, 但是仅对html做了展示处理,
如果有这样的需求:
  1. 图片需要动态控制大小;
  2. 图片点击后可以查看大图;
  3. 如果有多张图片, 点击后进入多图浏览界面, 且点进去即是当前图片位置;
这就需要我们能做到以下几点:
  1. 展示图片( 设置图片大小) 的代码可控;
  2. 可以监听图片点击事件;
  3. 点击图片时可以获取点击的图片url及该图片在全部图片中的position;
那么我们先来看HtmlSpanner对img是如何处理的:
找到项目中类: ImageHanler.java
public class ImageHandler extends TagNodeHandler {@ Override public void handleTagNode(TagNode node, SpannableStringBuilder builder, int start, int end, SpanStack stack) { String src = node.getAttributeByName(" src" ); builder.append(" \\uFFFC" ); Bitmap bitmap = loadBitmap(src); if (bitmap != null) { Drawable drawable = new BitmapDrawable(bitmap); drawable.setBounds(0, 0, bitmap.getWidth() - 1, bitmap.getHeight() - 1); stack.pushSpan( new ImageSpan(drawable), start, builder.length() ); } }/** * Loads a Bitmap from the given url. * * @ param url * @ return a Bitmap, or null if it could not be loaded. */ protected Bitmap loadBitmap(String url) { try { return BitmapFactory.decodeStream(new URL(url).openStream()); } catch (IOException io) { return null; } } }

在handleTagNode方法中我们可以获取到图片的url, 并得到了bitmap, 有了bitmap那么我们就可以根据bitmap获取图片宽高并动态调整大小了;
drawable.setBounds(0, 0, bitmap.getWidth() - 1,bitmap.getHeight() - 1);

传入计算好的宽高即可;
对于img的点击事件, 需要用到TextView的一个方法: setMovementMethod()及一个类: LinkMovementMethod; 此时的点击事件不再是view.OnclickListener了, 而是通过LinkMovementMethod类中的onTouch事件进行判断的:
@ Override public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { int action = event.getAction(); if (action = = MotionEvent.ACTION_UP || action = = MotionEvent.ACTION_DOWN) { int x = (int) event.getX(); int y = (int) event.getY(); x -= widget.getTotalPaddingLeft(); y -= widget.getTotalPaddingTop(); x + = widget.getScrollX(); y + = widget.getScrollY(); Layout layout = widget.getLayout(); int line = layout.getLineForVertical(y); int off = layout.getOffsetForHorizontal(line, x); ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class); if (link.length != 0) { if (action = = MotionEvent.ACTION_UP) { link[0].onClick(widget); } else if (action = = MotionEvent.ACTION_DOWN) { Selection.setSelection(buffer, buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0])); }return true; } else { Selection.removeSelection(buffer); } }return super.onTouchEvent(widget, buffer, event); }

我们知道img标签转化后的最终归宿是ImageSpan, 因此我们判断buffer.getSpans为ImageSpan时即点击了图片, 捕获了点击不算完事, 我们需要一个点击事件的回调啊, 因此我们需要重写LinkMovementMethod来完成回调( 回调方法有多种, 我这里用了一个handler) :
package net.nightwhistler.htmlspanner; import android.os.Handler; import android.os.Message; import android.text.Layout; import android.text.Selection; import android.text.Spannable; import android.text.method.LinkMovementMethod; import android.text.method.MovementMethod; import android.view.KeyEvent; import android.view.MotionEvent; import android.widget.TextView; public class LinkMovementMethodExt extends LinkMovementMethod { private static LinkMovementMethod sInstance; privateHandler handler = null; privateClass spanClass = null; public staticMovementMethod getInstance(Handler _handler,Class _spanClass) { if (sInstance = = null) { sInstance = new LinkMovementMethodExt(); ((LinkMovementMethodExt)sInstance).handler = _handler; ((LinkMovementMethodExt)sInstance).spanClass = _spanClass; }return sInstance; }int x1; int x2; int y1; int y2; @ Override public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { int action = event.getAction(); if (event.getAction() = = MotionEvent.ACTION_DOWN){ x1 = (int) event.getX(); y1 = (int) event.getY(); }if (event.getAction() = = MotionEvent.ACTION_UP) { x2 = (int) event.getX(); y2 = (int) event.getY(); if (Math.abs(x1 - x2) < 10 & & Math.abs(y1 - y2) < 10) {x2 -= widget.getTotalPaddingLeft(); y2 -= widget.getTotalPaddingTop(); x2 + = widget.getScrollX(); y2 + = widget.getScrollY(); Layout layout = widget.getLayout(); int line = layout.getLineForVertical(y2); int off = layout.getOffsetForHorizontal(line, x2); Object[] spans = buffer.getSpans(off, off, spanClass); if (spans.length != 0) { if (spans[0] instanceof MyImageSpan){ Selection.setSelection(buffer, buffer.getSpanStart(spans[0]), buffer.getSpanEnd(spans[0])); Message message = handler.obtainMessage(); message.obj = spans[0]; message.what = 2; message.sendToTarget(); } return true; } } }//return false; return super.onTouchEvent(widget, buffer, event); }public boolean canSelectArbitrarily() { return true; }public boolean onKeyUp(TextView widget, Spannable buffer, int keyCode, KeyEvent event) { return false; } }

注意里面的这部分代码:
if (spans[0] instanceof MyImageSpan)

MyImageSpan是什么鬼? 重写的ImageSpan吗? 对了就是重写的ImageSpan! 为什么要重写呢? 我们在通过handler发送ImageSpan并接收到后我们需要通过ImageSpan获取img的url, 但此时通过ImageSpan的gerSource( ) 并不能获取到, 所以我们就要重写一下ImageSpan, 在创建ImageSpan时就把url set进去:
/** * Created by byl on 2016-12-9. */public class MyImageSpan extends ImageSpan{public MyImageSpan(Context context, Bitmap b) { super(context, b); }public MyImageSpan(Context context, Bitmap b, int verticalAlignment) { super(context, b, verticalAlignment); }public MyImageSpan(Drawable d) { super(d); }public MyImageSpan(Drawable d, int verticalAlignment) { super(d, verticalAlignment); }public MyImageSpan(Drawable d, String source) { super(d, source); }public MyImageSpan(Drawable d, String source, int verticalAlignment) { super(d, source, verticalAlignment); }public MyImageSpan(Context context, Uri uri) { super(context, uri); }public MyImageSpan(Context context, Uri uri, int verticalAlignment) { super(context, uri, verticalAlignment); }public MyImageSpan(Context context, @ DrawableRes int resourceId) { super(context, resourceId); }public MyImageSpan(Context context, @ DrawableRes int resourceId, int verticalAlignment) { super(context, resourceId, verticalAlignment); }private String url; public String getUrl() { return url; }public void setUrl(String url) { this.url = url; }

同时在ImageHandler类的handleTagNode方法中也要替换ImageSpan:
MyImageSpan span= new MyImageSpan(drawable); span.setUrl(src); stack.pushSpan( span, start, builder.length() );

最终的实现流程为:
new Thread(new Runnable() { @ Override public void run() { final Spannable spannable = htmlSpanner.fromHtml(html); runOnUiThread(new Runnable() { @ Override public void run() { tv.setText(spannable); tv.setMovementMethod(LinkMovementMethodExt.getInstance(handler, ImageSpan.class)); } }); } }).start();

final Handler handler = new Handler() { public void handleMessage(Message msg) { switch (msg.what) { case 1://获取图片路径列表 String url = (String) msg.obj; Log.e(" jj" , " url> > " + url); imglist.add(url); break; case 2://图片点击事件 int position= 0; MyImageSpan span = (MyImageSpan) msg.obj; for (int i = 0; i < imglist.size(); i+ + ) { if (span.getUrl().equals(imglist.get(i))) { position = i; break; } } Log.e(" jj" ," position> > " + position); Intent intent= new Intent(MainActivity.this,ImgPreviewActivity.class); Bundle b= new Bundle(); b.putInt(" position" ,position); b.putStringArrayList(" imglist" ,imglist); intent.putExtra(" b" ,b); startActivity(intent); break; } }; };

好了, 现在就差点击图片浏览大图( 包括多图浏览) 了, 上面的handler中, 当msg.what为1时传来的即是图片路径, 这个是在哪里发送的呢? 当然是解析html获取到img标签时啦! 在ImageHanlder里:
public class ImageHandler extends TagNodeHandler {Context context; Handler handler; int screenWidth ; public ImageHandler() { }public ImageHandler(Context context,int screenWidth, Handler handler) { this.context= context; this.screenWidth= screenWidth; this.handler= handler; }@ Override public void handleTagNode(TagNode node, SpannableStringBuilder builder,int start, int end, SpanStack stack) { int height; String src = node.getAttributeByName(" src" ); builder.append(" \\uFFFC" ); Bitmap bitmap = loadBitmap(src); if (bitmap != null) { Drawable drawable = new BitmapDrawable(bitmap); if(screenWidth!= 0){ Message message = handler.obtainMessage(); message.obj = src; message.what = 1; message.sendToTarget(); height= screenWidth*bitmap.getHeight()/bitmap.getWidth(); drawable.setBounds(0, 0, screenWidth,height); }else{ drawable.setBounds(0, 0, bitmap.getWidth() - 1,bitmap.getHeight() - 1); } MyImageSpan span= new MyImageSpan(drawable); span.setUrl(src); stack.pushSpan( span, start, builder.length() ); }}/** * Loads a Bitmap from the given url. * * @ param url * @ return a Bitmap, or null if it could not be loaded. */ protected Bitmap loadBitmap(String url) { try { return BitmapFactory.decodeStream(new URL(url).openStream()); } catch (IOException io) { return null; } } }

screenWidth变量 和Handler对象都是这在初始化ImageHanlder时传入的, 初始化ImageHanlder的地方在HtmlSpanner类的registerBuiltInHandlers()方法中:
if(context!= null){ registerHandler(" img" , new ImageHandler(context,screenWidth,handler)); }else{ registerHandler(" img" , new ImageHandler()); }

因此, 在ImageHanlder中获取到img的url时就通过handler将其路径发送到主界面存储起来, 点击的时候通过比较url得到该图片的position, 并和图片列表imglist传入浏览界面即可!
需要注意的是, 如果html代码中有图片则需要网络权限, 并且加载时需要在线程中…
demo下载地址: http://download.csdn.net/detail/baiyuliang2013/9706568
ps: 如觉得使用handler稍显麻烦, 则可以在LinkMovementMethodExt中写一个自定义接口作为点击回调:
public interface ClickImgListener { void clickImg(String url); }

Object[] spans = buffer.getSpans(off, off, ImageSpan.class); if (spans.length != 0) { if (spans[0] instanceof MyImageSpan) { Selection.setSelection(buffer,buffer.getSpanStart(spans[0]),buffer.getSpanEnd(spans[0])); if(clickImgListener!= null)clickImgListener.clickImg(((MyImageSpan)spans[0]).getUrl()); } return true; }

在ImageHanler中, 声明一个变量private ArrayList imgList; 来存放img的url:
1.private ArrayList< String> imgList; 2.this.bitmapList = new ArrayList< > (); 3.public ArrayList< String> getImgList() { return imgList; } 4.imgList.add(src);

最终实现:
HtmlSpanner htmlSpanner = new HtmlSpanner(context); new Thread(() -> { final Spannable spannable = htmlSpanner.fromHtml(html); runOnUiThread(() -> { textView.setText(spannable); textView.setMovementMethod(new LinkMovementMethodExt((url) -> clickImg(url, htmlSpanner.getImageHandler().getImgList()))); }); }).start(); void clickImg(String url, ArrayList< String> imglist) { //点击事件处理 }

另外: 如果html中图片过多且过大, 很可能在这部分导致内存溢出:
bitmap = BitmapFactory.decodeStream(new URL(src).openStream());

可以使用这种方法来降低内存占用:
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); bitmapOptions.inSampleSize = 4; bitmap= BitmapFactory.decodeStream(new URL(src).openStream(), null, bitmapOptions);

【安卓TextView完美展示html格式代码】当然这会影响图片显示的清晰度, 好在有点击查看原图功能, 算是一种补偿吧, 也可根据具体业务具体对待!

    推荐阅读