實現智慧讀報(逐字朗讀+自動滾屏)
序言
最近在研究讀報的功能,想實現自動閱讀,即能朗讀,還能提示讀到什麼地方,反正是越方便越好。通過多次試驗終於成功了。實現了逐字朗讀變色,自動滾屏,螢幕常亮等功能。接下來你們將我再一次見識我的聰明才智。
效果
逐字朗讀,和自動滾屏
實現
1.逐字朗讀
先從朗讀開始,朗讀實現採用的是科大訊飛的語言合成技術。
在使用語言合成的時候可以設定一個合成監聽器,監聽器有如下的回撥。
public interface SynthesizerListener {
void onSpeakBegin();
void onBufferProgress(int var1, int var2, int var3, String var4);
void onSpeakPaused();
void onSpeakResumed();
void onSpeakProgress(int var1, int var2, int var3);
void onCompleted(SpeechError var1);
void onEvent(int var1, int var2, int var3, Bundle var4);
}
大家可能覺得只要在onSpeakProgress拿到進度就能逐字顯示了,那麼你們就太too young too simple了。因為官方文件這樣說的。
在onSpeakProgress有兩個位置,一個是beginPos,一個是endPos這兩個位置在朗讀同一句話的時候是不會變的變得是第一個引數也就是progress。而progress是整段文字的進度,不是閱讀一句話的進度。而且onSpeakProgress並不是讀了一個字就回調一次,它回撥的次數是不確定的。所以使用這個方法,我們可以輕鬆的實現整句的變色,但是想實現逐字的變色還差得遠,這個時候就不得不誇一下我的機智了(每天我不誇自己幾遍我就渾身難受 (~ ̄▽ ̄)~)。
下面談談我的思路,我原來寫過音樂播放器,裡面有歌詞顯示功能,歌詞檔案一般長這個樣子的。
一個是歌詞出現的時間點,一個歌詞的內容。我在想如果我有一份語言檔案的歌詞檔案就好了。但是我沒有,可是這難得到我嗎,顯然是不可能的。既然沒有歌詞檔案,那麼我們自己就記錄歌詞檔案,那麼記錄什麼內容了,其實只需要記錄兩個量就行了。一個是這句話在整段文字中開始的index,還有就是這句話每個字的平均時間就行了。程式碼如下:
//合成監聽器
private SynthesizerListener mSynListener = 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 percent, int beginPos, int endPos) {
//更新文字顏色
updateText(beginPos, endPos);
//自動滾屏
autoScroll(beginPos);
}
@Override
public void onCompleted(SpeechError speechError) {
if (!haveRecord) {
//記錄最後一句話的時間
int size = content.length() - lastBegin;
int avTime = (int) (System.currentTimeMillis() - lastTime) / size;
readTimeMap.put(lastBegin, avTime);
haveRecord = true;
}
reset();
}
@Override
public void onEvent(int i, int i1, int i2, Bundle bundle) {
}
};
private void reset() {
//重置
mHandler.stopUpdate();
btn_read.setText("開始讀報");
styled.removeSpan(spanRed);
tv_content.setText(styled);
scroll_news.smoothScrollTo(0, 0);
lastBegin = -1;
}
private void updateText(int beginPos, int endPos) {
if (lastBegin != beginPos) {
if (beginPos == 0) {
//第一次,記錄開始時間
lastTime = System.currentTimeMillis();
}
styled.removeSpan(spanRed);
if (!haveRecord) {
//整句更新
styled.setSpan(spanRed, beginPos, endPos, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv_content.setText(styled);
if (beginPos != 0) {
//從第二句開始,記錄上一句的平均用時
//計算平均用時
long now = System.currentTimeMillis();
long useTime = now - lastTime;
int averageTime = (int) (useTime / (beginPos - lastBegin));
Log.i("zzz", "save begin=" + lastBegin + " avTime=" + averageTime);
readTimeMap.put(lastBegin, averageTime);
lastTime = now;
}
} else {
//逐字更新
Message msg = new Message();
msg.what = MSG_UPDATE;
msg.arg1 = beginPos;
msg.arg2 = endPos;
mHandler.sendMessage(msg);
}
lastBegin = beginPos;
}
}
我用了一個boolean的haveRecord變數標識是否生成了歌詞檔案,如果沒有的話也就是第一遍播放的時候,就使用整段的顯示,同時記錄生成歌詞檔案。否則的話就在每句話開始的時候使用一個handler去獲取這句話每個字的平均閱讀時間然後自動更新文字。
long lastTime = 0;
int lastBegin = -1;
//是否記錄過時間線
boolean haveRecord = false;
Map<Integer, Integer> readTimeMap = new HashMap<>();
private static final int MSG_UPDATE = 1;
private static final int MSG_UPDATE_LOOP = 2;
UpdateHandler mHandler = new UpdateHandler();
class UpdateHandler extends Handler {
boolean needUpdate = false;
int begin = 0;
int end = 0;
int avTime = 0;
int index = 0;
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_UPDATE:
index=0;
needUpdate = true;
//起始位置
begin = msg.arg1;
//結束位置
end = msg.arg2;
//平均用時
Integer integer = readTimeMap.get(begin);
if (integer == null || integer == 0) {
needUpdate = false;
} else {
avTime = integer;
sendEmptyMessage(MSG_UPDATE_LOOP);
}
break;
case MSG_UPDATE_LOOP:
if (!needUpdate) {
removeMessages(MSG_UPDATE_LOOP);
return;
}
index++;
int newEnd = begin + index;
if (newEnd > end) {
stopUpdate();
} else {
styled.setSpan(spanRed, begin, newEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv_content.setText(styled);
sendEmptyMessageDelayed(MSG_UPDATE_LOOP, avTime);
}
break;
}
}
public void stopUpdate() {
needUpdate = false;
removeMessages(MSG_UPDATE_LOOP);
}
}
大家可能覺得這樣不方便,每一次顯示逐字閱讀的時候都還需要先讀一遍,主要是因為這只是用來測試的APP,如果在真實環境中完全可以在伺服器生成語言檔案,同時生成歌詞檔案,到時候客戶端就能直接逐字閱讀了,也不需要再去請求科大訊飛了。
3.自動滾屏
自動滾屏的實現就比較簡單了,主要是通過TextView的Layout實現的。通過呼叫TextView.getLayout()就能獲取到。
/**
* @return the Layout that is currently being used to display the text.
* This can be null if the text or width has recently changes.
*/
public final Layout getLayout() {
return mLayout;
}
但是需要注意的是獲取時機,可能會為null。
可以採用下面這種方式獲取的
final ViewTreeObserver vto = tv_content.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mLayout = tv_content.getLayout();
tv_content.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
在onSpeakProgress中更新完文字顏色就開始自動滾屏。
@Override
public void onSpeakProgress(int percent, int beginPos, int endPos) {
//更新文字顏色
updateText(beginPos, endPos);
//自動滾屏
autoScroll(beginPos);
}
而在自動滾屏的時候需要判斷這一句話有沒有引起,當前閱讀行數的變化,因為可能一句話很短,它和上一句在同一行,這種情況就不需要滾屏。
// 自動滾屏相關程式碼
Layout mLayout;
int lastLine = 0;
private void autoScroll(int beginPos) {
int line = getLine(beginPos);
//如果行數發生變化
if (line != lastLine) {
//保持有3行在最上面,正在朗讀的文字在中間,符合人們的視線。
if (line >= 3) {
scroll_news.smoothScrollTo(0, tv_content.getTop() + mLayout.getLineTop(line - 3));
}
lastLine = line;
}
}
private int getLine(int staPos) {
int lineNumber = 0;
if (mLayout != null) {
int line = mLayout.getLineCount();
for (int i = 0; i < line - 1; i++) {
if (staPos <= mLayout.getLineStart(i)) {
lineNumber = i;
break;
}
}
}
return lineNumber;
}
難度不大,主要是先關API的使用。
3.螢幕常亮
一個實用的功能,也在這裡記錄一下。
首先需要宣告許可權
<!--螢幕常亮-->
<uses-permission android:name="android.permission.WAKE_LOCK" />
在onCreate()的時候獲取
PowerManager.WakeLock mWakeLock;
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag");
接著和生命週期方法聯合使用
@Override
protected void onResume() {
super.onResume();
mWakeLock.acquire();
}
@Override
protected void onPause() {
super.onPause();
mWakeLock.release();
}
原始碼
很多細節的處理還是要看原始碼的
總結
研究這個功能,讓我又一次對自己的聰明才智充滿了信心。