最近有一個需求:移動端須要展現用戶在PC端作的筆記,而筆記內容是富文本形式——有圖片,有文字,文字能夠設置顏色、加粗、傾斜等等。同時,用戶點擊的時候可以語音朗讀所點擊的當前整句的內容。html
第一反應就是富文本!PC端生成的就是html文件,創給我,直接用WebView展現不就ok了嘛!android
可是,還有一需求:點擊斷句——咱們須要判斷用戶的點擊,定位到所點擊的整句話,而後再將整句內容實現語音播報。git
這樣的話WebView彷佛就不知足要求了,因此最終決定使用TextView來實現。github
靜態展現: bash
點擊斷句 微信
語音合成播報 這個就不展現了,你們能夠下載實例代碼運行體驗。網絡
特別地:我還實現了斷點語音播報和循環播報。框架
在實現上述須要求,咱們須要如下技術點爲基礎: 異步
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類型的富文本。
上一部分也說了,使用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(... )))。
這裏就用到了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());
}
複製代碼
看下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,給文字更改顏色。
播放一句完後發送消息播放下一句。
這樣就結束了哦!
能夠關注個人微信公衆號——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();
}
}
複製代碼