安卓TextView完美展示html格式代码
对于TextView展示html格式代码,最简单的办法就是使用textview.setText(Html.fromHtml(html));,即便其中有img标签,我们依然可以使用ImageGetter,和TagHandler对其中的图片做处理,但用过的都知道,效果不太理想,甚至无法满足产品简单的需求,那么今天博主就来为大家提供一个完美的解决方案!
html代码示例:
效果图: 
首先,要介绍一个开源项目,因为本篇博客所提供的方案是基于这个项目并进行扩展的: 
 https://github.com/NightWhistler/HtmlSpanner
该项目对html格式代码(内部标签和样式)基本提供了所有的转化方案,效果还是蛮不错的,但对于图片的处理仅做了展示,而对大小设置,点击事件等并未给出解决方案,所以本篇博客即是来对其进行扩展完善,满足日常开发需求!
首先,看HtmlSpanner的使用方法(注:HtmlSpanner内部代码实现不做详细分析,有兴趣的可下载项目研究):
textView.setText(htmlSpanner.fromHtml(html));
htmlSpanner.fromHtml(html)返回的是Spannable格式数据,使用非常简单,但是仅对html做了展示处理, 
如果有这样的需求:
- 图片需要动态控制大小;
 - 图片点击后可以查看大图;
 - 如果有多张图片,点击后进入多图浏览界面,且点进去即是当前图片位置;
 
这就需要我们能做到以下几点:
- 展示图片(设置图片大小)的代码可控;
 - 可以监听图片点击事件;
 - 点击图片时可以获取点击的图片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;
    private  Handler handler = null;
    private  Class spanClass = null;
    public static  MovementMethod 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格式代码的更多相关文章
- Vue之展示PDF格式的文档
		
事实上有很多种在前端展示PDF格式文档的方法,小编也用过好多种,例如有<iframe>.<embed>和<object>这些标签,但是在Vue项目里,这些方法都不能 ...
 - C# listview展示表格格式
		
有时候我们需要展示表格格式的数据,首先想到的是用datagridview控件,比如更改datagridview某一行的数据,这样操作起来就比较麻烦,而listview属于轻量级,刷新和更改相对来说效率 ...
 - 日期格式代码出现两次的错误 ORA-01810
		
错误的原因是使用了两次MM . 一.Oracle中使用to_date()时格式化日期需要注意格式码 如:select to_date('2005-01-01 11:11:21','yyyy-MM-dd ...
 - 解决安卓TextView异常换行,参差不齐等问题
		
参考:http://blog.csdn.net/u012286242/article/details/28429267?utm_source=tuicool&utm_medium=referr ...
 - ORA-01810: 格式代码出现两次
		
今天在修改SQL语句的时候遇到这个小问题,提示的还是比较明显的,当然解决之道我是从百度上摘取的! 错误语句段:AND V.UPLOAD_DATE <=TO_DATE ('2013-11-11 2 ...
 - Sublime Text 3 格式代码插件 codeFormatter
		
一款可以对html.JS.CSS.PHP.python代码格式化的sublime插件 默认快捷键ctrl+alt+F,默认可以对html.js.css格式代码, 如果想对PHP格式化,需要PHP5.6 ...
 - ORA-01810格式代码出现两次 的解决方案
		
今早做一个查询页面时,需要查询两个时间区间的跨度,使用TO_DATE函数,一开始写成了Sql代码 TO_DATE('2014-08-04 00:00:00','YYYY-MM-DD HH:mm:ss' ...
 - ORA-01810:格式代码出现两次 解决方法
		
在写一个sql插入数据库的时候 to_date('20140509131034','yyyyMMddHHmmss') 报ORA-01810:格式代码出现两次 原因是java中的年月日和oracle中的 ...
 - 格式代码出现两次oracle
		
报ORA-01810:格式代码出现两次 原因是Java中的年月日和Oracle中的年月日表示形式不一样 oracle用MI来代表分钟,而不是java中的mm
 
随机推荐
- [USACO07NOV]牛继电器Cow Relays
			
题目描述 给出一张无向连通图,求S到E经过k条边的最短路. 输入输出样例 输入样例#1: 2 6 6 4 11 4 6 4 4 8 8 4 9 6 6 8 2 6 9 3 8 9 输出样例#1: 10 ...
 - ●洛谷P2934 [USACO09JAN]安全出行Safe Travel
			
题链: https://www.luogu.org/problemnew/show/P2934 题解: 最短路(树),可并堆(左偏堆),并查集. 个人感觉很好的一个题. 由于题目已经明确说明:从1点到 ...
 - UVALive - 3938:"Ray, Pass me the dishes!"
			
优美的线段树 #include<cstdio> #include<cstdlib> #include<algorithm> #include<cstring& ...
 - [bzoj1041][HAOI2008]圆上的整点
			
我能想得出怎么做才奇怪好吗 题解:http://blog.csdn.net/csyzcyj/article/details/10044629 #include<iostream> #inc ...
 - angularjs中关于跨域设置白名单
			
在config中注入$sceDelegateProvider服务使用resourceUrlWhitelist([])方法添加白名单 跨域时将method的属性设置为"jsonp"就 ...
 - font-spider利器对webfont网页字体压缩使用
			
http://font-spider.org/ npm install font-spider -g hyheilizhitij(汉仪黑荔枝体简) //引入 @font-face{ font-fami ...
 - jvm(四):垃圾回收
			
垃圾回收我们主要从以下三个方面进行描述 垃圾对象的判断 目前判断对象为垃圾对象有两种方法:引用计数法,可达性分析法,目前普遍是的是可达性分析法 可达性分析法的实现原理: 定义gcroot一直往下找,如 ...
 - jQuery 选择器 prop() 和attr()
			
Day30 jQuery 1.1.1.1 什么是jQuery? n jQuery是javaScript的前端框架.对常见的对象和常用的方法进行封装,使用更方便. 它兼容CSS3,还兼容各种浏览器.文档 ...
 - JQ简单实现无缝滚动
			
$(function(){ $("ul li:lt(5)").clone().appendTo("ul"); var $width = $("ul l ...
 - Spring boot 整合 Mybatis + Thymeleaf开发web(二)
			
上一章我把整个后台的搭建和逻辑给写出来了,也贴的相应的代码,这章节就来看看怎么使用Thymeleaf模板引擎吧,Spring Boot默认推荐Thymeleaf模板,之前是用jsp来作为视图层的渲染, ...