用TextView实现富文本展现,点击断句和语音播报

最近有一个需求:移动端须要展现用户在PC端作的笔记,而笔记内容是富文本形式——有图片,有文字,文字能够设置颜色、加粗、倾斜等等。同时,用户点击的时候可以语音朗读所点击的当前整句的内容。html

第一反应就是富文本!PC端生成的就是html文件,创给我,直接用WebView展现不就ok了嘛!android

可是,还有一需求:点击断句——咱们须要判断用户的点击,定位到所点击的整句话,而后再将整句内容实现语音播报。git

这样的话WebView彷佛就不知足要求了,因此最终决定使用TextView来实现。github

github地址 欢迎star **csdn地址 **数组

1、先看下富文本展现效果:

静态展现: bash

这里写图片描述

点击断句 微信

这里写图片描述

语音合成播报 这个就不展现了,你们能够下载实例代码运行体验。网络

特别地:我还实现了断点语音播报和循环播报。框架

2、技术点

在实现上述须要求,咱们须要如下技术点为基础: 异步

这里写图片描述

3、Html.fromHtml( )

fromHtml重载两个方法,分别是:

一、Spanned android.text.Html.fromHtml(String source) //输入的参数为(html格式的文本)

目前android不支持所有的html的标签,目前只支持与文本显示和段落等标签,对于图片和其余的多媒体,还有一些自定义标签不能识别

例子:

TextView t3 = (TextView) findViewById(R.id.text3);   
t3.setText(Html.fromHtml( "<b>text3:</b> Text with a " + "<a href=\"http://www.google.com\">link</a> " +"created in the Java source code using HTML."));
复制代码

2 、Spanned android.text.Html.fromHtml(String source, ImageGetter imageGetter, TagHandler tagHandler)

  • source: 需处理的html文本

  • **imageGetter :**对图片处理(处理html中的图片标签)

  • **tagHandler :**对标签进行处理(至关于自定义的标签处理,在这里面能够处理自定义的标签)

也就是说,咱们彻底可使用Html.fromHtml方法,传入html代码,最后返回Spanned 对象,在使用setText方法既可实现用TextView展现html类型的富文本。

4、图片处理

上一部分也说了,使用Html.fromHtml( )方法展现富文本的时候,某些自定义的标签和图片识别不了,也就是加载不出来。而咱们的项目中没有自定义的特殊标签,最关键的就是图片的加载!

翻过头咱们再看下fromHtml的三个参数的方法:

  • source: 需处理的html文本

  • **imageGetter :**对图片处理(处理html中的图片标签)

  • **tagHandler :**对标签进行处理(至关于自定义的标签处理,在这里面能够处理自定义的标签)

source是html文本这个不用说了,第二个参数imageGetter 负责图片的加载,tagHandler 是在加载时获取各标签。

想到这里,图片加载使用自定义ImageGetter就能够了啊,因而乎:

一、 建立图片请求工具方法:

html标签中的图片全是在img标签中,并且都是图片连接,因此简单写一方法来实现加载网络图片:

/**
     * 根据一个网络链接(String)获取bitmap图像
     *
     * @param imageUri
     * @return
     */
    public static Bitmap getbitmap(String imageUri) {

        // 显示网络上的图片
        Bitmap bitmap = null;
        try {
            URL myFileUrl = new URL(imageUri);
            HttpURLConnection conn = (HttpURLConnection) myFileUrl
                    .openConnection();
            conn.setDoInput(true);
            conn.connect();
            InputStream is = conn.getInputStream();
            bitmap = BitmapFactory.decodeStream(is);
            is.close();

        } catch (OutOfMemoryError e) {
            e.printStackTrace();
            bitmap = null;
        } catch (IOException e) {
            e.printStackTrace();
            bitmap = null;
        }
        return bitmap;
    }
复制代码

我这里简单使用HttpUrlConnection来实现加载网络图片,你们能够根据本身项目换成Glide等框架。

二、自定义ImageLoader:

class NetWorkImageGetter implements Html.ImageGetter {

        @Override
        public Drawable getDrawable(final String source) {

            Log.e(TAG, "getDrawable: ");

            Drawable drawable= new BitmapDrawable(getbitmap(source));

            return drawable;
        }

    }
复制代码

getDrawable方法中的参数source经过打log看出就是在加载html文本时,须要加载的网络图片的地址url;

那彷佛很简单啊,加载网络图片返回(须要注意的是:加载到的是Bitmap对象,须要转成Drawable对象再返回;再者就是须要考虑子线程去加载,我这里只是简单展现原理,没有开启子线程加载图片)。

而后建立NetWorkImageGetter 对象,在fromHtml时传入既可。

可是!

三、存在的问题及优化

这样存在一个问题,咱们使用fromHtml加载html文本时,图片是同步加载,而加载网络图片和加载html是异步的,也就是说:在加载到图片以前,其余文本已经显示到界面上,因此须要咱们再次设置html文本。

那咱们考虑下,是否是每加载完一张图片就刷新一下呢?这样会致使界面刷新好屡次,用户可能刚滑到底部查看内容,这时加载到第一张图片,界面就会立马刷新到最上方,这样的用户体验会不会很很差~

因此,个人思路是当全部图片所有加载完成后,再刷新界面,也就是从新setText

但我怎么会知道何时就所有加载完图片了呢?或者说我怎么可以知道一共须要加载多少张图片呢?

此时就用到了第三个参数:TagHandler

先了解下TagHandler

new Html.TagHandler() {
	@Override
	public void handleTag(boolean b, String s, Editable editable, XMLReader xmlReader) {
		Log.e(TAG, "handleTag: " + s);
	}
};
复制代码

结果呢:

这里写图片描述

忽然发现,s变量就是html文本中的各个标签。同时咱们也发现,每次都是先加载图片,而后才弹回img的tag。

这样就好办了,

在TagHandler中计算img标签的个数,在ImageGetter中等加载图片个数所有完成时,再次刷新界面(从新调用setText方法)。

setText(Html.fromHtml(text, mNetWorkImageGetter, new Html.TagHandler() {
	@Override
	public void handleTag(boolean b, String s, Editable editable, XMLReader xmlReader) {
		Log.e(TAG, "handleTag: " + s);
		if (s.equals("img")) {
			img_num++;
		}
	}
}));
复制代码
class NetWorkImageGetter implements Html.ImageGetter {

        @Override
        public Drawable getDrawable(final String source) {

            Log.e(TAG, "getDrawable: ");

            if (imgs.containsKey(source)) {
                imgs.get(source).setBounds(0, 0, imgs.get(source).getIntrinsicWidth() * 2,
                        imgs.get(source).getIntrinsicHeight() * 2);
            } else {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        imgs.put(source, new BitmapDrawable(getbitmap(source)));

                        if (imgs.size() == img_num) {
                            handler.post(new Runnable() {
                                @Override
                                public void run() {
                                    setText();
                                }
                            });
                        }
                    }
                }).start();
            }

            return imgs.get(source);
        }

    }
复制代码

在所有图片加载完成后在刷新textview内容(这里的setText是稍后会讲到的封装的设置html代码,你们可简单的理解成setText(Html.fromHtml(... )))

5、点击断句

这里就用到了SpannableStringBuilder

个人思路是这样的:

这里写图片描述

private void setText() {
        Log.e(TAG, "setText: ");
        lines = getText().toString().split("。|?|!|@|···|;|;|!");

        if (lines != null && lines.length > 0) {

            span = new int[lines.length];
            for (int i = 0; i < lines.length; i++) {
                Log.e(TAG, "run: " + i + " " + lines[i]);
                if (i == 0) {
                    span[i] = 0;
                } else {
                    span[i] = span[i - 1] + lines[i - 1].length() + 1;
                }

            }

        }

        setText(Html.fromHtml(text, mNetWorkImageGetter, null));

        style = new SpannableStringBuilder(getText());
        for (int i = 0; i < span.length; i++) {
            if (i == span.length - 1) {
                style.setSpan(new TextViewURLSpan(i), span[i], getText().length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            } else {
                style.setSpan(new TextViewURLSpan(i), span[i], span[i + 1] - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            }

        }
        setText(style);
        setMovementMethod(LinkMovementMethod.getInstance());
    }
复制代码
  1. 从TextView获取展现的内容。咱们认为! 。 ? @ ... ···等符号是一句话结束的标志,因此经过它们将完整语句分割,存入数组;
  2. 建立一int类型数组,存放每句话在全文中开始的位置;
  3. 使用循环将每一句都设置对应的点击;
  4. 注意setMovementMethod(LinkMovementMethod.getInstance());必须设置,不然无效果。

看下TextViewURLSpan代码:

private class TextViewURLSpan extends ClickableSpan {
        int flag;

        public TextViewURLSpan(int flag) {
            this.flag = flag;
        }

        @Override
        public void updateDrawState(TextPaint ds) {
        }

        @Override
        public void onClick(View widget) {//点击事件
            Log.e(TAG, "onClick: ");

            handler.removeMessages(205);

            startSpeaking(flag);
        }
    }
复制代码

咱们将每句对应数组中的下标传入,方便语音合成时从数组中获取文本内容。

由于循环播放是使用handler发消息进行通知的,因此从新开始播放时,先移出以前的消息。

6、语音播放

private void startSpeaking(final int flag) {
        for (int i = 0; i < span.length; i++) {
            if (i == flag) {
                if (i == span.length - 1) {
                    style.setSpan(new ForegroundColorSpan(Color.RED), span[i], getText().length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                } else {
                    style.setSpan(new ForegroundColorSpan(Color.RED), span[i], span[i + 1] - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            } else {
                if (i == span.length - 1) {
                    style.setSpan(new ForegroundColorSpan(Color.GRAY), span[i], getText().length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                } else {
                    style.setSpan(new ForegroundColorSpan(Color.GRAY), span[i], span[i + 1] - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            }
        }

        setText(style);

        // 语音合成
        mSpeechSynthesizer.setParameter(SpeechConstant.ENGINE_TYPE, mEngineType);
        mSpeechSynthesizer.setParameter(SpeechConstant.ENGINE_MODE, mEngineType);

        mSpeechSynthesizer.setParameter(SpeechConstant.VOICE_NAME, voicerCloud);

        mSpeechSynthesizer.startSpeaking(lines[flag], new SynthesizerListener() {
            @Override
            public void onSpeakBegin() {

            }

            @Override
            public void onBufferProgress(int i, int i1, int i2, String s) {

            }

            @Override
            public void onSpeakPaused() {

            }

            @Override
            public void onSpeakResumed() {

            }

            @Override
            public void onSpeakProgress(int i, int i1, int i2) {

            }

            @Override
            public void onCompleted(SpeechError speechError) {
                if (flag != lines.length - 1) {
                    Message msg = new Message();
                    msg.what = 205;
                    msg.obj = flag;
                    handler.sendMessage(msg);


                }
            }

            @Override
            public void onEvent(int i, int i1, int i2, Bundle bundle) {

            }
        });
    }
复制代码

语音合成就再也不啰嗦了,不清楚的查看讯飞开发文档就ok了,挺简单的。

由于需求要求是点击每句要变颜色,因此进行了一次循环,给每句话都设置了ForegroundColorSpan,给文字更改颜色。

播放一句完后发送消息播放下一句。

这样就结束了哦!

能够关注个人微信公众号——Android机动车,获取更多精彩内容!

最后附上完整代码:

/**
 * Description: 富文本展现  讯飞语音阅读
 * Created by jia on 2017/10/20.
 * 人之因此能,是相信能
 */
public class RichTextView extends TextView {

    private static final String TAG = "RichTextView";

    private HashMap<String, Drawable> imgs = new HashMap<>();

    private NetWorkImageGetter mNetWorkImageGetter = new NetWorkImageGetter();

    private int img_num = 0;

    private int[] span;

    private String[] lines;

    private String text;

    private SpannableStringBuilder style;

    //语音合成对象
    private SpeechSynthesizer mSpeechSynthesizer;

    // 默认云端发音人
    public static String voicerCloud = "xiaoyan";
    // 引擎类型
    private String mEngineType = SpeechConstant.TYPE_CLOUD;


    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 205) {
                startSpeaking((int) msg.obj + 1);
            }
        }
    };

    public RichTextView(Context context) {
        super(context);
        init();
    }

    public RichTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public RichTextView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {

        mSpeechSynthesizer = SpeechSynthesizer.createSynthesizer(getContext(), new InitListener() {
            @Override
            public void onInit(int i) {
                Log.e(TAG, "onInit: " + i);
            }
        });
    }

    public void fromHtml(String text) {

        this.text = text;

        setText(Html.fromHtml(text, mNetWorkImageGetter, new Html.TagHandler() {
            @Override
            public void handleTag(boolean b, String s, Editable editable, XMLReader xmlReader) {
                Log.e(TAG, "handleTag: " + s);
                if (s.equals("img")) {
                    img_num++;
                }
            }
        }));

        // 没有图片直接加载
        if (img_num == 0) {
            setText();
        }
    }


    class NetWorkImageGetter implements Html.ImageGetter {

        @Override
        public Drawable getDrawable(final String source) {

            Log.e(TAG, "getDrawable: ");

            if (imgs.containsKey(source)) {
                imgs.get(source).setBounds(0, 0, imgs.get(source).getIntrinsicWidth() * 2,
                        imgs.get(source).getIntrinsicHeight() * 2);
            } else {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        imgs.put(source, new BitmapDrawable(getbitmap(source)));

                        if (imgs.size() == img_num) {
                            handler.post(new Runnable() {
                                @Override
                                public void run() {
                                    setText();
                                }
                            });
                        }
                    }
                }).start();
            }

            return imgs.get(source);
        }

    }

    private void setText() {
        Log.e(TAG, "setText: ");
        lines = getText().toString().split("。|?|!|@|···|;|;|!");

        if (lines != null && lines.length > 0) {

            span = new int[lines.length];
            for (int i = 0; i < lines.length; i++) {
                Log.e(TAG, "run: " + i + " " + lines[i]);
                if (i == 0) {
                    span[i] = 0;
                } else {
                    span[i] = span[i - 1] + lines[i - 1].length() + 1;
                }

            }

        }

        setText(Html.fromHtml(text, mNetWorkImageGetter, null));

        style = new SpannableStringBuilder(getText());
        for (int i = 0; i < span.length; i++) {
            if (i == span.length - 1) {
                style.setSpan(new TextViewURLSpan(i), span[i], getText().length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            } else {
                style.setSpan(new TextViewURLSpan(i), span[i], span[i + 1] - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            }

        }
        setText(style);
        setMovementMethod(LinkMovementMethod.getInstance());
    }

    private class TextViewURLSpan extends ClickableSpan {
        int flag;

        public TextViewURLSpan(int flag) {
            this.flag = flag;
        }

        @Override
        public void updateDrawState(TextPaint ds) {
        }

        @Override
        public void onClick(View widget) {//点击事件
            Log.e(TAG, "onClick: ");

            handler.removeMessages(205);

            startSpeaking(flag);
        }
    }

    private void startSpeaking(final int flag) {
        for (int i = 0; i < span.length; i++) {
            if (i == flag) {
                if (i == span.length - 1) {
                    style.setSpan(new ForegroundColorSpan(Color.RED), span[i], getText().length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                } else {
                    style.setSpan(new ForegroundColorSpan(Color.RED), span[i], span[i + 1] - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            } else {
                if (i == span.length - 1) {
                    style.setSpan(new ForegroundColorSpan(Color.GRAY), span[i], getText().length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                } else {
                    style.setSpan(new ForegroundColorSpan(Color.GRAY), span[i], span[i + 1] - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
                }
            }
        }

        setText(style);

        // 语音合成
        mSpeechSynthesizer.setParameter(SpeechConstant.ENGINE_TYPE, mEngineType);
        mSpeechSynthesizer.setParameter(SpeechConstant.ENGINE_MODE, mEngineType);

        mSpeechSynthesizer.setParameter(SpeechConstant.VOICE_NAME, voicerCloud);

        mSpeechSynthesizer.startSpeaking(lines[flag], new SynthesizerListener() {
            @Override
            public void onSpeakBegin() {

            }

            @Override
            public void onBufferProgress(int i, int i1, int i2, String s) {

            }

            @Override
            public void onSpeakPaused() {

            }

            @Override
            public void onSpeakResumed() {

            }

            @Override
            public void onSpeakProgress(int i, int i1, int i2) {

            }

            @Override
            public void onCompleted(SpeechError speechError) {
                if (flag != lines.length - 1) {
                    Message msg = new Message();
                    msg.what = 205;
                    msg.obj = flag;
                    handler.sendMessage(msg);


                }
            }

            @Override
            public void onEvent(int i, int i1, int i2, Bundle bundle) {

            }
        });
    }

    /**
     * 根据一个网络链接(String)获取bitmap图像
     *
     * @param imageUri
     * @return
     */
    public static Bitmap getbitmap(String imageUri) {

        // 显示网络上的图片
        Bitmap bitmap = null;
        try {
            URL myFileUrl = new URL(imageUri);
            HttpURLConnection conn = (HttpURLConnection) myFileUrl
                    .openConnection();
            conn.setDoInput(true);
            conn.connect();
            InputStream is = conn.getInputStream();
            bitmap = BitmapFactory.decodeStream(is);
            is.close();

        } catch (OutOfMemoryError e) {
            e.printStackTrace();
            bitmap = null;
        } catch (IOException e) {
            e.printStackTrace();
            bitmap = null;
        }
        return bitmap;
    }

    @Override
    protected boolean getDefaultEditable() {//禁止EditText被编辑
        return false;
    }


    @Override
    protected MovementMethod getDefaultMovementMethod() {
        return super.getDefaultMovementMethod();
    }

    @Override
    public void setVisibility(int visibility) {
        super.setVisibility(visibility);
        mSpeechSynthesizer.stopSpeaking();
    }
}

复制代码
相关文章
相关标签/搜索