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

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

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

这样的话WebView似乎就不满足要求了,所以最终决定使用TextView来实现。

一、先看下富文本展示效果:

静态展示:

这里写图片描述

点击断句

这里写图片描述

语音合成播报

这个就不展示了,大家可以下载实例代码运行体验。

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

二、技术点

在实现上述需要求,我们需要以下技术点为基础:

这里写图片描述

三、Html.fromHtml( )

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

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

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

例子:

TextView t3 = (TextView) findViewById(R.id.text3);

t3.setText(Html.fromHtml( "text3: Text with a " + "link " +"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类型的富文本。

四、图片处理

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

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

source: 需处理的html文本

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

tagHandler :对标签进行处理(相当于自定义的标签处理,在这里面可以处理自定义的标签)

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

想到这里,图片加载使用自定义ImageGetter就可以了啊,于是乎:

1、 创建图片请求工具方法:

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等框架。

2、自定义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时传入既可。

但是!

3、存在的问题及优化

这样存在一个问题,我们使用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(... )))。

五、点击断句

这里就用到了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());

}

从TextView获取展示的内容。我们认为! 。 ? @ ... ···等符号是一句话结束的标志,所以通过它们将完整语句分割,存入数组;

创建一int类型数组,存放每句话在全文中开始的位置;

使用循环将每一句都设置对应的点击;

注意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发消息进行通知的,所以重新开始播放时,先移出之前的消息。

六、语音播放

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,给文字更改颜色。

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

这样就结束了哦!

可以关注我的微信公众号——安卓干货营,获取更多精彩内容!

这里写图片描述

最后附上完整代码:

/**

* Description: 富文本展示 讯飞语音阅读

* Created by jia on 2017/10/20.

* 人之所以能,是相信能

*/

public class RichTextView extends TextView {

private static final String TAG = "RichTextView";

private HashMap 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();

}

}

C语言对文本进行断句,用TextView实现富文本展示,点击断句和语音播报相关推荐

  1. android如何展示富文本_Android中如何在textView实现富文本

    怎么在textView中实现类似这样的文本??要求可以点击跳转. 代码如下: xml中: android:id="@+id/tv_one" android:layout_width ...

  2. html富文本编辑器插件_vue中使用vuequilleditor富文本编辑器

    点击上方"小姚同学技术栈"快速关注我哟! vue-quill-editor是一个基于quill.适用于vue的富文本编辑器开源项目,支持服务端渲染和单页应用.目前项目热度还算可以, ...

  3. kind富文本编辑器_在项目中集成富文本编辑器

    前   言 现在学程序的都离不开 Markdown 语法了吧,Markdown 已经成为典型的转换为HTML的非正式规范和参考实现,现在市场上也出现了许多Markdown实现,在基本语法之上额外增加了 ...

  4. android富文本图片自适应,Android 图片混排富文本编辑器控件

    一.一个Android 图片混排富文本编辑器控件(仿兴趣部落) 1.1 图片混排富文本控件 是一种图片和文字混合在一起的控件,文本之间可以插入图片,类似于网页的排版样式. 1.2 该控件主要是仿兴趣部 ...

  5. Andoid TextView显示富文本html内容及问题处理

    目录 富文本内容与效果 TextView + Html ImageGetter 处理图片(表情) TagHandler 处理html内容的节点 Html的转换过程 HtmlToSpannedConve ...

  6. 安卓文本编辑器php cpp,开源的Android富文本编辑器

    RichEditor 基于原生EditText+span实现的Android富文本编辑器 github地址:https://github.com/yuruiyin/RichEditor 组件描述 该组 ...

  7. 富文本转换字符串 php,php 如何将一个富文本字符串生成word文档?

    问题描述 我现在有一个富文本字符串, 比如$str=" qqq啊啊啊啊啊百度网址 ": 中间我省去了大量的图片base64的编码. 问题出现的环境背景及自己尝试过哪些方法 相关代码 ...

  8. selenium python 文本框输入信息_selenium python向富文本框中输入内容

    基于本人不会JavaScript,不能像大神一样写出很牛X的方法,只能使用者屌丝方法了,不过很容易理解. 我使用的是ueditor富文本框 1.我的富文本框是在一个iframe中,进入iframe的方 ...

  9. html表单控件富文本框,表单控件之富文本框实践

    多行文本输入框(不是富文本框)的html代码如下: 学历及经历: ${emp.details} 富文本的js代码如下: // create Editor from textarea HTML elem ...

最新文章

  1. ACM题集以及各种总结大全(转)
  2. 高性能计算的线程模型:Pthreads 还是 OpenMP
  3. Ribbon为什么要加入点对点直连的功能?如何操作?两句话玩转!
  4. 海量数据库的查询优化及分页算法方案(3)--改善SQL语句[转]
  5. 减法运算的借位标志cf_数学|有理数运算法则及题型汇总
  6. layui实现后台表格数据显示--学生管理系统(layui搜索,删除,批量删除,增加,修改,php接口后台)
  7. network 关于PV,网站访问量和服务器带宽的选择
  8. NMEA-0183通信协议
  9. 网易云ncm文件转mp3
  10. DID去中心化身份认证技术调研
  11. java玫瑰花代码_给爱人的玫瑰花表白程序代码–Java版 | 学步园
  12. Docker服务,堆栈和分布式应用程序捆绑
  13. C# 反双曲余弦函数
  14. Apache POI操作PPT: 文字替换 图片替换 表格填充 PPT合并
  15. MAK代理激活的使用方法和注意事项
  16. 1. 遥控器-华科尔Devo7e 改造成为支持 dsm2/dsmx 的多制式遥控器
  17. 使用Beep()函数发出指定音高 (一)
  18. Win11磁盘扩展卷变成灰色无法点击解决方法
  19. 罗技G304接收器无反应问题,现象及解决方法
  20. 红外图像处理:去竖条

热门文章

  1. html5设置锚点,Markdown也不服输
  2. RabbitMQ(六)——Spring boot中消费消息的两种方式
  3. 红米AC3000、小米cr8806、8808、8809成功刷入openwrt
  4. CVPR 2022 Oral | MLP进军底层视觉!谷歌提出MAXIM:刷榜多个图像处理任务,代码已开源!...
  5. d盘格式化后怎么恢复
  6. 分享 | 日置3561电池测试仪调零/自校准详解教程
  7. android全平台基于ffmpeg解码本地MP4视频推流到RTMP服务器
  8. groupby后选取列和不选取列的区别
  9. excel统计类别个数
  10. 仿照微信写的uni-app项目