Android TTS实现简单阅读器

时间:2023-01-09 13:59:02



本文原创版权归 51CTO winorlose2000 所有,转载请按如下方式于文章显示位置详细标明原创作者及出处,以示尊重!!

作者:winorlose2000

​​


 


Android TTS实现简单阅读器

         简单的Txt文本阅读器,主要用于介绍Google Android的TTS接口。


 


一、 TTS


         在package android.speech.tts内,主要阅读下TextToSpeech.OnInitListener、TextToSpeech. OnUtteranceCompletedListener两个接口和TextToSpeech、TextToSpeech.Engine两个类。


         具体还是自己去看下SDK文档吧。(我也是完整阅读过了的^^)


 


二、 TTS 引擎


         以前在网上的例子,或者就我《 ​​Android基础样例​​》里的中文TTS例子,都是eSpeak引擎实现的。这种方式是要用其封装的TTS接口,再通过下载TTS数据使用。


         而Android的SDK中还提供了TTS服务的接口,用于供应商提供服务的。也就是语音合成服务商只管提供它的服务,开发者只管使用Android的TTS接口,用户自己安装想要的服务自己进行选择。


 


         总之呢,我用的是讯飞语音TTS v1.0。有两个文件,一个是Service程序,一个是语音数据。下载网址:​​http://soft.shouji.com.cn/down/22160.html​


 


1 )关于讯飞(貌似广告?)


         好吧,少说点了,它也提供了个开发者平台。如下:


         讯飞语音云: ​​http://dev.voicecloud.cn/download.php?vt=1​


 


         有试了下它那语音分析,话说,弹出的框框能不能好看点啊。(做个小话筒就好了么T^T)


         恩,还有,现在讯飞是要开始宣传了么?貌似3月22日什么开发者大会-_-!(又广告了?)


 


2 )其他中文引擎


         参见文章:Android中文语音合成(TTS)各家引擎对比。(原网址打不开==,另外的网址就不贴了,搜下吧)


 


三、阅读器工程


         现在学乖了,直接贴些代码得了==。代码中注释应该满清晰详细了^^。


 


1 )界面布局


         布局由main.xml includes header.xml & footer.xml组成,并写有了定时收起等。


TtsFatherActivity.java


1. /**
2. * 1)本类用于main.xml的布局控制。子类再去实现各控件的TTS相关功能。
3. * 2)用继承方式实现是为了利用布局中控件的onClick属性(懒得多写代码==!)。
4. */
5. public abstract class TtsFatherActivity extends
6.
7. private GestureDetector gd; // 手势检测
8. private GlobalUtil globalUtil; // 全局公用类
9.
10. private ScrollView scrollView; // 滚动视图
11. private LinearLayout headerLayout, footerLayout; // 顶部、底部布局
12. private TextView textView; // 文本标签
13.
14. private static final long ANIM_DURATION = 500; // 动画时间(毫秒)
15.
16. private static final int DIALOG_TEXT_LIST = 0; // 文本列表对话框id
17. private final String[] textPaths = new String[] { "one.txt", "two.txt",
18. "浏览..." }; // assets内文本资源路径
19.
20. protected String textTitle; // 文本标题
21. protected String textContent; // 文本内容
22.
23. private Timer timer; // 计时器
24. private static final long TIMEOUT = 2000; // 超时时间
25. private static final int TIMER_LAYOUT_OUT = 1; // 布局收起
26.
27. private boolean isLayoutOut = false; // 布局收起状态
28.
29. /** Handler处理操作 */
30. public Handler mHandler = new
31. @Override
32. public void
33. switch
34. case
35. /* headerLayout收起动画 */
36. globalUtil.startTransAnim(headerLayout,
37. GlobalUtil.AnimMode.UP_OUT, ANIM_DURATION);
38. headerLayout.setVisibility(View.GONE);
39. /* footerLayout收起动画 */
40. globalUtil.startTransAnim(footerLayout,
41. GlobalUtil.AnimMode.DOWN_OUT, ANIM_DURATION);
42. footerLayout.setVisibility(View.GONE);
43. true; // 重置布局收起状态
44. break;
45. }
46. }
47. };
48.
49. @Override
50. public void
51. super.onCreate(savedInstanceState);
52. setContentView(R.layout.main);
53.
54. new GestureDetector(new MySimpleGesture()); // 手势检测处理
55. // 获取全局公用类
56.
57. // 获取滚动视图
58. // 获取顶部布局
59. // 获取底部布局
60. textView = (TextView) findViewById(R.id.textView);
61.
62. 0); // 默认显示“上邪.txt”
63. // 定时收起布局
64. }
65.
66. /** 使用GestureDetector检测手势(ScrollView内也需监听时的方式) */
67. @Override
68. public boolean
69. gd.onTouchEvent(ev);
70. scrollView.onTouchEvent(ev);
71. return super.dispatchTouchEvent(ev);
72. }
73.
74. /** onCreateDialog */
75. @Override
76. protected Dialog onCreateDialog(int
77. switch
78. case
79. return new AlertDialog.Builder(this).setItems(textPaths,
80. new
81. @Override
82. public void onClick(DialogInterface dialog, int
83. if (2
84. // 跳转到文件浏览Activity
85. new
86. this,
87. class),
88. FileBrowserActivity.CODE_FILE_BROWSER);
89. else
90. // 设置文本内容
91. }
92. }
93. }).create();
94. }
95. return super.onCreateDialog(id);
96. }
97.
98. @Override
99. protected void onActivityResult(int requestCode, int
100. if
101. if
102. // 获得文件名称
103. String filename = data.getExtras().getString(
104. FileBrowserActivity.KEY_FILENAME);
105. this.textTitle = filename;
106. try
107. // FileInputStream fis = new FileInputStream(
108. // new File(filename));
109. // // FileInputStream不支持mark/reset操作,不该直接这样
110. // String encoding = globalUtil.getIsEncoding(fis);
111. // textContent = globalUtil.is2Str(fis, encoding);
112. /**
113. * TXT简单判断编码类型后转字符串
114. *
115. * ps:
116. * 1)扯淡,3.58MB的txt读出来了==
117. * 看来需要转成BufferedReader以readLine()方式读好些啊
118. *
119. * 2)TextView将大文本全部显示,这貌似...
120. *
121. * 时间主要花费在文本显示过程,不改进了,暂时将就吧==
122. * 2.1)用View自定义个控件显示文本也蛮久的,未减少多少时间。
123. * 2.2)至于AsyncTask,文本显示还是要在UI线程的==。
124. *
125. * 如果我们要仿个阅读器,用View自定义个控件还是必须的。
126. * 1)分段读取大文本,可以考虑3段(前后两段用于缓冲)
127. * 根据滑屏&显示内容等,注意文本显示衔接。
128. * 2)滚动条可以外面套个ScrollView。由各属性判断出大文本需要显示的高度,
129. * 重写onMeasure用setMeasuredDimension()设置好,才会有滚动条。
130. * 当然自己用scrollTo()、scrollBy()实现动画也是好的。
131. * 3)至于其他选中当前行啊什么的,慢慢写就成了...
132. *
133. * 不知道大家还有什么好的想法没?
134. */
135. // long time1 = System.currentTimeMillis();
136. new
137. new
138. // long time2 = System.currentTimeMillis();
139. // Log.e("TAG1", "==" + (time2 - time1) + "==");
140. textView.setText(textContent);
141. // long time3 = System.currentTimeMillis();
142. // Log.e("TAG1", "==" + (time3 - time2) + "==");
143. catch
144. textView.setText(R.string.text_error);
145. "";
146. }
147. }
148. }
149. }
150.
151. /** 设置文本内容 */
152. private void setText(int
153. this.textTitle = textPaths[textIndex];
154. try
155. textContent = globalUtil.is2Str(getAssets().open(textTitle),
156. "UTF-8");
157. textView.setText(textContent);
158. catch
159. textView.setText(R.string.text_error);
160. "";
161. }
162. }
163.
164. /** 定时收起布局(已定时时重新开始定时) */
165. protected void
166. if (null
167. timer.cancel();
168. }
169. new
170. // 超时TIMEOUT退出
171. new
172. @Override
173. public void
174. mHandler.sendEmptyMessage(TIMER_LAYOUT_OUT);
175. }
176. }, TIMEOUT);
177. }
178.
179. /** 自定义手势类 */
180. private class MySimpleGesture extends
181.
182. /** 双击第二下 */
183. @Override
184. public boolean
185. if
186. /* headerLayout进入动画 */
187. headerLayout.setVisibility(View.VISIBLE);
188. globalUtil.startTransAnim(headerLayout,
189. GlobalUtil.AnimMode.UP_IN, ANIM_DURATION);
190. /* footerLayout进入动画 */
191. footerLayout.setVisibility(View.VISIBLE);
192. globalUtil.startTransAnim(footerLayout,
193. GlobalUtil.AnimMode.DOWN_IN, ANIM_DURATION);
194. // 定时收起布局
195. false; // 重置布局收起状态
196. else
197. /* headerLayout退出动画 */
198. globalUtil.startTransAnim(headerLayout,
199. GlobalUtil.AnimMode.UP_OUT, ANIM_DURATION);
200. headerLayout.setVisibility(View.GONE);
201. /* footerLayout退出动画 */
202. globalUtil.startTransAnim(footerLayout,
203. GlobalUtil.AnimMode.DOWN_OUT, ANIM_DURATION);
204. footerLayout.setVisibility(View.GONE);
205. // 取消定时收起动画
206. if (null
207. timer.cancel();
208. }
209. true; // 重置布局收起状态
210. }
211. return false;
212. }
213.
214. /** 长按屏幕时 */
215. @Override
216. public void
217. // 显示文本列表对话框
218. showDialog(DIALOG_TEXT_LIST);
219. }
220. }
221.
222. }

 



 


2 )TTS 控制


         音量&语速控制也写了的^^。


 


TtsSampleActivity.java


1. public class TtsSampleActivity extends TtsFatherActivity implements
2. OnSeekBarChangeListener, TextToSpeech.OnInitListener,
3. TextToSpeech.OnUtteranceCompletedListener {
4.
5. // private static final String TAG = "TtsSampleActivity"; // 日志标记
6.
7. private AudioManager audioManager; // 音频管理对象
8.
9. // TTS音量类型(AudioManager.STREAM_MUSIC = AudioManager.STREAM_TTS = 11)
10. private static final int
11.
12. private TextToSpeech mTts; // TTS对象
13. private static final int REQ_CHECK_TTS_DATA = 110; // TTS数据校验请求值
14. private boolean isSetting = false; // 进入设置标记
15. private boolean isRateChanged = false; // 速率改变标记
16. private boolean isStopped = false; // TTS引擎停止发声标记
17. private float mSpeechRate = 1.0f; // 朗读速率
18.
19. private SeekBar volumeBar, speedBar; // 音量&语速
20.
21. // 合成声音资源文件的路径
22. private static final String SAVE_DIR_PATH = "/sdcard/AndroidTTS/";
23. private static final String SAVE_FILE_PATH = SAVE_DIR_PATH + "sound.wav";
24.
25. @Override
26. public void
27. super.onCreate(savedInstanceState);
28.
29. // 获得音频管理对象
30. audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
31.
32. /* volumeBar */
33. volumeBar = (SeekBar) findViewById(R.id.volumeBar);
34. this);
35. // 由当前音量设置进度(需保证进度上限=音频上限=15,否则按比例设置)
36. volumeBar.setProgress(audioManager.getStreamVolume(STREAM_TTS));
37.
38. /* speedBar */
39. speedBar = (SeekBar) findViewById(R.id.speedBar);
40. this);
41.
42. // 初始化文件夹路径
43. }
44.
45. /** saveFileBtn点击事件 */
46. public void
47. // 将文本合成声音资源文件
48. int
49. SAVE_FILE_PATH) ? R.string.synt_success : R.string.synt_fail;
50. this, resId, Toast.LENGTH_SHORT).show(); // Toast提示
51. // 重新定时收起布局
52. }
53.
54. /** playFileBtn点击事件 */
55. public void
56. // 播放指定的使用文件
57. // 重新定时收起布局
58. }
59.
60. /** stopBtn点击事件 */
61. public void
62. // 停止当前发声
63. // 重新定时收起布局
64. }
65.
66. /** playBtn点击事件 */
67. public void
68. // tts合成语音播放
69. // 重新定时收起布局
70. }
71.
72. /** settingBtn点击事件 */
73. public void
74. // 跳转到“语音输入与输出”设置界面&设置标志位
75. isSetting = toTtsSettings();
76. // 重新定时收起布局
77. }
78.
79. /** SeekBar进度改变时 */
80. @Override
81. public void onProgressChanged(SeekBar seekBar, int
82. boolean
83. switch
84. case
85. // 由设置当前TTS音量(需保证进度上限=音频上限=15,否则按比例设置)
86. 0);
87. break;
88. case
89. /* 需要重新绑定TTS引擎,速度在onInit()里设置 */
90. true; // 速率改变标记
91. // 最大值为20时,以下方式计算为0.5~2倍速
92. 10) ? (progress / 10f)
93. 0.5f + progress / 20f);
94. // 校验TTS引擎安装及资源状态,重新绑定引擎
95. checkTtsData();
96. break;
97. }
98. // 重新定时收起布局
99. }
100.
101. /** SeekBar开始拖动时 */
102. @Override
103. public void
104. }
105.
106. /** SeekBar结束拖动时 */
107. @Override
108. public void
109. }
110.
111. /**
112. * TTS引擎初始化时回调方法
113. *
114. * 引擎相关参数(音量、语速)等都需在这设置。
115. * 1)创建完成后再去设置,会有意外的效果^^
116. * 2)音量也可由AudioManager进行控制(和音乐一个媒体流类型)
117. */
118. @Override
119. public void onInit(int
120. if
121. // 设置朗读速率
122. // 设置发声合成监听,注意也需要在onInit()中做才有效
123. this);
124. if
125. // tts合成语音播放
126. false; // 重置标记位
127. }
128. }
129. }
130.
131. /**
132. * TTS引擎完成发声完成时回调方法
133. *
134. * 1)stop()取消时也会回调
135. * 2)需在onInit()内设置接口
136. * 3)utteranceId由speak()时的请求参数设定
137. * 参数key:TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID
138. */
139. @Override
140. public void onUtteranceCompleted(final
141. /* 测试该接口的Toast提示 */
142. new
143. @Override
144. public void
145. int
146. : R.string.utte_completed;
147. // 提示文本发生完成
148. Toast.makeText(getApplicationContext(),
149. getString(resId, utteranceId), Toast.LENGTH_SHORT)
150. .show();
151. }
152. });
153. }
154.
155. /** onActivityResult */
156. @Override
157. protected void onActivityResult(int requestCode, int
158. if
159. switch
160. case TextToSpeech.Engine.CHECK_VOICE_DATA_PASS: // TTS引擎可用
161. // 针对于重新绑定引擎,需要先shutdown()
162. if (null
163. // 停止当前发声
164. // 释放资源
165. }
166. new TextToSpeech(this, this); // 创建TextToSpeech对象
167. break;
168. case TextToSpeech.Engine.CHECK_VOICE_DATA_BAD_DATA: // 数据错误
169. case TextToSpeech.Engine.CHECK_VOICE_DATA_MISSING_DATA: // 缺失数据资源
170. case TextToSpeech.Engine.CHECK_VOICE_DATA_MISSING_VOLUME: // 缺少数据存储量
171. // 提示用户是否重装TTS引擎数据的对话框
172. break;
173. case TextToSpeech.Engine.CHECK_VOICE_DATA_FAIL: // 检查失败
174. default:
175. break;
176. }
177. }
178. super.onActivityResult(requestCode, resultCode, data);
179. }
180.
181. /** 校验TTS引擎安装及资源状态 */
182. private boolean
183. try
184. new
185. checkIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
186. startActivityForResult(checkIntent, REQ_CHECK_TTS_DATA);
187. return true;
188. catch
189. return false;
190. }
191. }
192.
193. /** 提示用户是否重装TTS引擎数据的对话框 */
194. private void
195. new AlertDialog.Builder(this).setTitle("TTS引擎数据错误")
196. "是否尝试重装TTS引擎数据到设备上?")
197. "是", new
198. @Override
199. public void onClick(DialogInterface dialog, int
200. // 触发引擎在TTS引擎在设备上安装资源文件
201. new
202. dataIntent
203. .setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
204. startActivity(dataIntent);
205. }
206. "否", null).show();
207. }
208.
209. /** 跳转到“语音输入与输出”设置界面 */
210. private boolean
211. try
212. new Intent("com.android.settings.TTS_SETTINGS"));
213. return true;
214. catch
215. return false;
216. }
217. }
218.
219. @Override
220. protected void
221. // 校验TTS引擎安装及资源状态
222. super.onStart();
223. }
224.
225. @Override
226. protected void
227. /* 从设置返回后重新绑定TTS,避免仍用旧引擎 */
228. if
229. // 校验TTS引擎安装及资源状态
230. false;
231. }
232. super.onResume();
233. }
234.
235. @Override
236. protected void
237. /* HOME键 */
238. // 停止当前发声
239. super.onStop();
240. }
241.
242. @Override
243. public void
244. /* BACK键 */
245. // 停止当前发声
246. // 释放资源
247. super.onBackPressed();
248. }
249.
250. /** tts合成语音播放 */
251. private int
252. if (null
253. false; // 设置标记
254. /**
255. * 叙述text。
256. *
257. * 1) 参数2(int queueMode)
258. * 1.1)QUEUE_ADD:增加模式。增加在队列尾,继续原来的说话。
259. * 1.2)QUEUE_FLUSH:刷新模式。中断正在进行的说话,说新的内容。
260. * 2)参数3(HashMap<String, String> params)
261. * 2.1)请求的参数,可以为null。
262. * 2.2)注意KEY_PARAM_UTTERANCE_ID。
263. */
264. new
265. params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, textTitle);
266. return
267. }
268. return
269. }
270.
271. // /** 判断TTS是否正在发声 */
272. // private boolean isSpeaking() {
273. // // 使用mTts.isSpeaking()判断时,第一次speak()返回true,多次就返回false了。
274. // return audioManager.isMusicActive();
275. // }
276.
277. /** 停止当前发声,同时放弃所有在等待队列的发声 */
278. private int
279. true; // 设置标记
280. return (null
281. }
282.
283. /** 释放资源(解除语音服务绑定) */
284. private void
285. if (null
286. mTts.shutdown();
287. }
288. }
289.
290. /** 初始化文件夹路径 */
291. private void initDirs(final
292. new
293. if
294. file.mkdirs();
295. }
296. }
297.
298. /** 将文本合成声音资源文件 */
299. private int ttsSaveFile(String text, final
300. return (null
301. null, filename);
302. }
303.
304. /** 播放指定的使用文件 */
305. private int ttsPlayFile(final
306. // 如果存在FILENAME_SAVE文件的话
307. if (new
308. try
309. /* 使用MediaPlayer进行播放(没进行控制==) */
310. new
311. player.setDataSource(filename);
312. player.prepare();
313. player.start();
314. return
315. catch
316. e.printStackTrace();
317. return
318. }
319. }
320. return
321. }
322.
323. }



  3)公共工具

         这个也贴出来吧==。



 



GlobalUtil.java



    1. /**
    2. * @brief 全局公用类
    3. * @details 各部分公用内容都较少,就丢一起了==。
    4. */
    5. public final class
    6.
    7. /** Bob Lee懒加载:内部类GlobalUtilHolder */
    8. static class
    9. static GlobalUtil instance = new
    10. }
    11.
    12. /** Bob Lee懒加载:返回GlobalUtil的单例 */
    13. public static
    14. return
    15. }
    16.
    17. /** 获取窗口默认显示信息 */
    18. public
    19. WindowManager wm = (WindowManager) mContext
    20. .getSystemService(Context.WINDOW_SERVICE);
    21. return
    22. }
    23.
    24. /** 动画方式 */
    25. public enum
    26. UP_IN, UP_OUT, DOWN_IN, DOWN_OUT, LEFT_IN, LEFT_OUT, RIGHT_IN, RIGHT_OUT
    27. };
    28.
    29. /**
    30. * @brief 横移或竖移动画
    31. *
    32. * @param v 移动视图
    33. * @param animMode 动画方式
    34. * @param durationMillis 持续时间
    35. */
    36. public void startTransAnim(View v, AnimMode animMode, long
    37. int w = v.getWidth(), h = v.getHeight(); // 获取移动视图宽高
    38. float fromXDelta = 0, toXDelta = 0, fromYDelta = 0, toYDelta = 0;
    39. switch
    40. case
    41. fromYDelta = -h;
    42. break;
    43. case
    44. toYDelta = -h;
    45. break;
    46. case
    47. fromYDelta = h;
    48. break;
    49. case
    50. toYDelta = h;
    51. break;
    52. case
    53. fromXDelta = -w;
    54. break;
    55. case
    56. toXDelta = -w;
    57. break;
    58. case
    59. fromXDelta = w;
    60. break;
    61. case
    62. toXDelta = w;
    63. break;
    64. }
    65. new
    66. // 位移动画
    67. // 设置时间
    68. // 开始动画
    69. }
    70.
    71. /**
    72. * @brief InputStream转为String
    73. *
    74. * @param is 输入流
    75. * @param encoding 编码方式
    76. * @return 字符串结果
    77. * @throws UnsupportedEncodingException 不支持的编码
    78. */
    79. public
    80. throws
    81. /*
    82. * 不直接从InputStream里读byte[],再转成String,以避免截断汉字。
    83. * 如utf8一个汉字3字节,用byte[1024]会截断末尾而乱码。
    84. */
    85. new
    86. /* 以char[]方式读取 */
    87. new
    88. try
    89. char[] b = new char[4096]; // 1024*4*2Byte
    90. for (int n; (n = isReader.read(b)) != -1;) {
    91. 0, n);
    92. }
    93. return
    94. catch
    95. e.printStackTrace();
    96. }
    97. return "";
    98. }
    99.
    100. /**
    101. * @brief 带BOM的文本的FileInputStream转为String,自动判断编码类型
    102. *
    103. * @param fis 文件输入流
    104. * @return 字符串结果
    105. * @throws UnsupportedEncodingException 不支持的编码
    106. */
    107. public
    108. throws
    109. // 转成BufferedInputStream
    110. new
    111. // 简单判断文本编码
    112. String encoding = getIsEncoding(bis);
    113. // 转成BufferedReader
    114. new BufferedReader(new
    115. encoding));
    116. /* 以readLine()方式读取 */
    117. new
    118. try
    119. for (String s; (s = reader.readLine()) != null;) {
    120. out.append(s);
    121. "\n");
    122. }
    123. return
    124. catch
    125. e.printStackTrace();
    126. }
    127. return "";
    128. }
    129.
    130. /**
    131. * @brief 带BOM的文本判断,否则认为GB2312(即ANSI类型的TXT)
    132. * @details 复杂文件编码检测,请google cpdetector!
    133. *
    134. * @param is InputStream
    135. * @return 编码类型
    136. *
    137. * @warning markSupported为true时才进行判断,否则返回默认GB2312。
    138. * \n FileInputStream不支持mark/reset操作;BufferedInputStream支持此操作。
    139. */
    140. public
    141. "GB2312";
    142. // Log.e("is.markSupported()", "==" + is.markSupported() + "==");
    143. if (is.markSupported()) { // 支持mark()
    144. try
    145. 5); // 打个TAG(5>3)
    146. byte[] head = new byte[3];
    147. is.read(head);
    148. if (head[0] == -1 && head[1] == -2)
    149. "UTF-16";
    150. if (head[0] == -2 && head[1] == -1)
    151. "Unicode";
    152. if (head[0] == -17 && head[1] == -69 && head[2] == -65)
    153. "UTF-8";
    154. // 返回TAG
    155. catch
    156. e.printStackTrace();
    157. }
    158. }
    159. return
    160. }
    161.
    162. }


     



    四、阅读器截图



    1 )进入画面.png



    Android TTS实现简单阅读器



     



    2 )长按屏幕.png



    Android TTS实现简单阅读器



     



    3 )自带two.txt.png



    Android TTS实现简单阅读器



     



    4 )“浏览… 操作”的截图



    Android TTS实现简单阅读器



    浏览...(1).png



     



    Android TTS实现简单阅读器



    浏览...(2).png



     



    Android TTS实现简单阅读器



    浏览...(3).png


     



    五、后记



             直接读大文本,后果自负啊。(没做控制呢T^T)

     



     



             ps:应用的入口图标 ​

    Android TTS实现简单阅读器

    ​ ,可爱吧^^。

     



    附件下载:




     >> Android TTS实现简单阅读器.zip


    作者:winorlose2000