android 项目练习:自己的词典app——生词本(一)
前言:
自学android差不多两个月了,由于本身对英语不感冒,而且记英语单词总是很快忘记,因此学习的过程也是蛮累的,好多类和方法都不知道啥意思,还要去查词典才知道。
还是延续我读书时的记忆方法——每次遇到生词就写在笔记本上,下次在遇到就算不记得中文意思,也能记得写过这个单词,然后就是找笔记本就可以了。不过那,这种方法也有个问题——自己的字太丑,每次都是找了好久都没找那个词,其实明明在哪里,只是快速扫看不到o(╯□╰)o。
后来,就想找一个背单词的app,可以把我不认识的生词添加到一个生词本,可以快速浏览生词本里的单词,也可以仅仅针对生词本里的词出一些帮助记忆的练习题?又想,既然我在学android,为什么不自己做一个那?于是就有了这个项目练习!
项目实现:
我在网上找到了一个前辈分享的类似的app编写过程,发现其中很多内容都是我会的,于是我就参考着自己动手写起来。
由于这个项目不是完成后才开始写这篇博客,是我边实践边写的,因此整体思路是根据我的写代码进度来的,在写这里的时候刚实现了查单词的界面和完整功能。
查词界面:
先来看下这个界面的功能和实现思路:
(一)肯定是要能查单词
简单的实现思路就是使用现有词典的API接口,我采用的是金山词霸的API接口,地址:http://open.iciba.com/。优点是这个接口会返回发音MP3的http地址。
查词接口:http://dict-co.iciba.com/api/dictionary.php?w=go&key=** 这里的key是你自己申请的金山词霸开放平台的API key。
打开后是这样的:
<dict num="219" id="219" name="219"><key>go</key><ps>gəʊ</ps><pron>http://res.iciba.com/resource/amp3/0/0/34/d1/34d1f91fb2e514b8576fab1a75a89a6b.mp3</pron><ps>goʊ</ps><pron>http://res.iciba.com/resource/amp3/1/0/34/d1/34d1f91fb2e514b8576fab1a75a89a6b.mp3</pron><pos>vi.</pos><acceptation>走;离开;去做;进行;</acceptation><pos>vt.</pos><acceptation>变得;发出…声音;成为;处于…状态;</acceptation><pos>n.</pos><acceptation>轮到的顺序;精力;干劲;尝试;</acceptation><sent><orig>Go is an irregular verb.</orig><trans>go是个不规则动词.</trans></sent><sent><orig>Kyong - go means a warning or half - point deduction and gam - jeom means a one - point deduction.</orig><trans>Kyoug -go是指一次警告或被扣减半分, gam -jeom是指被扣减1分.</trans></sent><sent><orig>From the get - go means from the beginning.</orig><trans>原来fromtheget-go 就是一开始的时候.</trans></sent><sent><orig>With the reduction of SRWC, GO activity decreased mild water stress and increased water stress.</orig><trans>随着土壤相对含水量的下降,GO酶括性在土壤水分含量下降时首先降低,以后又逐渐上升.</trans></sent><sent><orig>We proved orthocompactness and weakly suborthocompactness are equivalent for all subspaces of product of two GO - space.</orig><trans>证明了GO - 空间子空间的正交紧性和弱子正交紧性是等价的.</trans></sent></dict>
因此这里就要用到Http网络访问和XML解析。为避免重复访问网络,我们可以将解析出来单词的数据保存在本地,这样下次在查到该词是可以直接从本地读取了,同样的我们可以直接把MP3文件也保存本地。
分析完成后开始动手,首先按功能分模块,这方面由于我是新手,就是按照自己看的清晰的方式来,新建一个util包,这里都是放一些工具类。然后新建一个类HttpUtil,通过HttpURLConnection实现网络访问功能:
public class HttpUtil { /** * 在新线程中发送网络请求 * * @param address 网络地址 * @param listener HttpCallBackListener接口的实现类; * onFinish方法为访问成功后的回调方法; * onError为访问不成功时的回调方法 */ public static void sentHttpRequest(final String address, final HttpCallBackListener listener) { new Thread(new Runnable() { @Override public void run() { HttpURLConnection connection = null; try { URL url = new URL(address); connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); connection.setConnectTimeout(8000); connection.setReadTimeout(8000); InputStream inputStream = connection.getInputStream(); if (listener != null) { listener.onFinish(inputStream); } } catch (IOException e) { e.printStackTrace(); if (listener != null) { listener.onError(); } } finally { if (connection != null) { connection.disconnect(); } } } }).start(); }}
android网络访问不能在UI线程中进行,避免阻塞,因此,这里我直接在新线程实现,根据目的需要,完成网络请求后要对返回的XML文件进行解析,因此方法第二参数传入HttpCallBackListener接口的实现类,分别对应onFinish方法为访问成功后的回调方法,onError为访问不成功时的回调方法。
接下来是XML解析了,我采用的是SAX解析方法。
我们先分析下XML文件,看看有哪些节点:
key:单词本身; ps:第一个是英音音标,第二个是美音音标; pron第一个是英音的MP3地址,第二个是美音的;pos 词性; acception 词义;sent 例句; orig例句英语;trans例句中文翻译。
这个api接口也可以查中文,只需要在待查的词前面加上一个下划线 _ 即可,如 :_你好。
<dict num="219" id="219" name="219"><key>你好</key><fy>Hello</fy><sent><orig>Hello! Hello! Hello! Hello! Hel - lo!</orig><trans>你好! 你好! 你好! 你好! 你好!</trans></sent><sent><orig>Hello! Hello! Hello! Hello ! I'm glad to meet you.</orig><trans>你好! 你好! 你好! 你好! 见到你很高兴.</trans></sent><sent><orig>Hello Marie. Hello Berlioz. Hello Toulouse.</orig><trans>你好玛丽, 你好柏里欧, 你好图鲁兹.</trans></sent><sent><orig>B Hi Gao. How are you doing? It's good to meet you.</orig><trans>B你好,高. 你好 吗 ?很高兴认识你.</trans></sent><sent><orig>Grant: Hi , Tess. Hi , Jenna. Are you doing your homework?</orig><trans>格兰特: 你好! 苔丝. 你好! 詹娜. 你们在做家庭作业 吗 ?</trans></sent></dict>
可以看到查中文的话会多一个属性:fy 即中文的英文翻译,要一起考虑进去。
因为要把查到单词的内容保存本地,我们就要建一个Words类用来管理xml解析出来的内容,新建一个model包,在其下新建一个Words类:
public class Words { //中英文标记 private boolean isChinese; //要翻译的单词,可以是中文; private String key; //key为中文时的翻译 private String fy; //英音发音 private String psE; //英音发音的mp3地址 private String pronE; //美音发音 private String psA; //美音发音的mp3地址 private String pronA; //单词的词性与词义 private String posAcceptation; //例句 private String sent; public Words() { this.key = ""; this.fy = ""; this.psE = ""; this.pronE = ""; this.psA = ""; this.pronA = ""; this.posAcceptation = ""; this.sent = ""; this.isChinese = false; } public Words(boolean isChinese, String key, String fy, String psE, String pronE, String psA, String pronA, String posAcceptation, String sent) { this.isChinese = isChinese; this.key = key; this.fy = fy; this.psE = psE; this.pronE = pronE; this.psA = psA; this.pronA = pronA; this.posAcceptation = posAcceptation; this.sent = sent; } public boolean getIsChinese() { return isChinese; } public void setIsChinese(boolean isChinese) { this.isChinese = isChinese; } public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getFy() { return fy; } public void setFy(String fy) { this.fy = fy; } public String getPsE() { return psE; } public void setPsE(String psE) { this.psE = psE; } public String getPronE() { return pronE; } public void setPronE(String pronE) { this.pronE = pronE; } public String getPsA() { return psA; } public void setPsA(String psA) { this.psA = psA; } public String getPronA() { return pronA; } public void setPronA(String pronA) { this.pronA = pronA; } public String getPosAcceptation() { return posAcceptation; } public void setPosAcceptation(String posAcceptation) { this.posAcceptation = posAcceptation; } public String getSent() { return sent; } public void setSent(String sent) { this.sent = sent; }}
其中只包含一些成员变量,对应我们需要的内容,还有各自的get()和set()方法。
接着在util包下新建一个WordsHandler类继承自DefaultHandler,这个类中解析XML内容成一个words对象:
public class WordsHandler extends DefaultHandler { //记录当前节点 private String nodeName; private Words words; //单词的词性与词义 private StringBuilder posAcceptation; //例句 private StringBuilder sent; /** * 获取解析后的words对象 */ public Words getWords() { return words; } //开始解析XML时调用 @Override public void startDocument() throws SAXException { //初始化 words = new Words(); posAcceptation = new StringBuilder(); sent = new StringBuilder(); } //结束解析XML时调用 @Override public void endDocument() throws SAXException { //将所有解析出来的内容赋予words words.setPosAcceptation(posAcceptation.toString().trim()); words.setSent(sent.toString().trim()); } //开始解析节点时调用 @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { nodeName = localName; } //结束解析节点时调用 @Override public void endElement(String uri, String localName, String qName) throws SAXException { //在读完整个节点后换行 if ("acceptation".equals(localName)) { posAcceptation.append("\n"); } else if ("orig".equals(localName)) { sent.append("\n"); } else if ("trans".equals(localName)) { sent.append("\n"); sent.append("\n"); } } //获取节点中内容时调用 @Override public void characters(char[] ch, int start, int length) throws SAXException { String a = new String(ch, start, length); //去掉文本中原有的换行 for (int i = start; i < start + length; i++) { if (ch[i] == '\n') return; } //将节点的内容存入Words对象对应的属性中 if ("key".equals(nodeName)) { words.setKey(a.trim()); } else if ("ps".equals(nodeName)) { if (words.getPsE().length() <= 0) { words.setPsE(a.trim()); } else { words.setPsA(a.trim()); } } else if ("pron".equals(nodeName)) { if (words.getPronE().length() <= 0) { words.setPronE(a.trim()); } else { words.setPronA(a.trim()); } } else if ("pos".equals(nodeName)) { posAcceptation.append(a); } else if ("acceptation".equals(nodeName)) { posAcceptation.append(a); } else if ("orig".equals(nodeName)) { sent.append(a); } else if ("trans".equals(nodeName)) { sent.append(a); } else if ("fy".equals(nodeName)) { words.setFy(a); words.setIsChinese(true); } }}
在这里,如何对解析出来的文本重新排版换行这个问题卡了我好几个小时,最后终于找到解决方法,我在昨天的一篇博客中有分享:SAX解析中换行问题解决 。
接着,同样是util包下新建一个ParseXML类,作为解析XML的工具类:
public class ParseXML { /** * 使用SAX解析XML的方法 */ public static void parse(DefaultHandler handler, InputStream inputStream) { try { InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8"); BufferedReader reader = new BufferedReader(inputStreamReader); SAXParserFactory factory = SAXParserFactory.newInstance(); XMLReader xmlReader = factory.newSAXParser().getXMLReader(); xmlReader.setContentHandler(handler); xmlReader.parse(new InputSource(reader)); } catch (Exception e) { e.printStackTrace(); } }}
这里应该没什么问题,调用SAXParserFactory.newInstance()获得SAXParserFactory的实例,再调用newSAXParser().getXMLReader()获得XMLPreader实例,setContentHandler()传入自定义的解析类WordsHandler,最后调用parse()方法,传入inputStream包装成的BufferReader开始解析。
解析后我们可以调用WordsHandler的getWords获得所查单词对应的words对象,接下来可以用SQLite保存在本地。新建一个db包,在db包中新建一个WordsSQLiteOpenHelper类继承自SQLiteOpenHelper,在这个类中新建words库:
public class WordsSQLiteOpenHelper extends SQLiteOpenHelper { /**建表语句*/ private String CREATE_WORDS = "create table Words(id Integer primary key autoincrement," + "isChinese text,key text,fy text,psE text,pronE text,psA text,pronA text,posAcceptation text,sent text)"; public WordsSQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) { super(context, name, factory, version); } @Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_WORDS); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { }}
目前没有升级数据的需要,因此仅写了创建数据库的代码。
接下来是一个大类WordsAction,我把它放在util包下,这个类里包含了大部分查词界面所用到的方法,包括:保存words到数据库、获取address地址、向数据库中查找words、保存发音mp3文件、播放发音MP3。
发音MP3我们后面再看,先来看数据库这块:
public class WordsAction { /** * 本类的实例 */ private static WordsAction wordsAction; /** * Words的表名 */ private final String TABLE_WORDS = "Words"; /** * 数据库工具,用于增、删、该、查 */ private SQLiteDatabase db; private MediaPlayer player = null; /** * 私有化的构造器 */ private WordsAction(Context context) { WordsSQLiteOpenHelper helper = new WordsSQLiteOpenHelper(context, TABLE_WORDS, null, 1); db = helper.getWritableDatabase(); } /** * 单例类WordsAction获取实例方法 * * @param context 上下文 */ public static WordsAction getInstance(Context context) { //双重效验锁,提高性能 if (wordsAction == null) { synchronized (WordsAction.class) { if (wordsAction == null) { wordsAction = new WordsAction(context); } } } return wordsAction; } /** * 向数据库中保存新的Words对象 * 会先对word进行判断,为有效值时才会保存 * * @param words 单词类的实例 */ public boolean saveWords(Words words) { //判断是否是有效对象,即有数据 if (words.getSent().length() > 0) { ContentValues values = new ContentValues(); values.put("isChinese", "" + words.getIsChinese()); values.put("key", words.getKey()); values.put("fy", words.getFy()); values.put("psE", words.getPsE()); values.put("pronE", words.getPronE()); values.put("psA", words.getPsA()); values.put("pronA", words.getPronA()); values.put("posAcceptation", words.getPosAcceptation()); values.put("sent", words.getSent()); db.insert(TABLE_WORDS, null, values); values.clear(); return true; } return false; } /** * 从数据库中查找查询的words * * @param key 查找的值 * @return words 若返回words的key为空,则说明数据库中没有该词 */ public Words getWordsFromSQLite(String key) { Words words = new Words(); Cursor cursor = db.query(TABLE_WORDS, null, "key=?", new String[]{key}, null, null, null); //数据库中有 if (cursor.getCount() > 0) { Log.d("测试", "数据库中有"); if (cursor.moveToFirst()) { do { String isChinese = cursor.getString(cursor.getColumnIndex("isChinese")); if ("true".equals(isChinese)) { words.setIsChinese(true); } else if ("false".equals(isChinese)) { words.setIsChinese(false); } words.setKey(cursor.getString(cursor.getColumnIndex("key"))); words.setFy(cursor.getString(cursor.getColumnIndex("fy"))); words.setPsE(cursor.getString(cursor.getColumnIndex("psE"))); words.setPronE(cursor.getString(cursor.getColumnIndex("pronE"))); words.setPsA(cursor.getString(cursor.getColumnIndex("psA"))); words.setPronA(cursor.getString(cursor.getColumnIndex("pronA"))); words.setPosAcceptation(cursor.getString(cursor.getColumnIndex("posAcceptation"))); words.setSent(cursor.getString(cursor.getColumnIndex("sent"))); } while (cursor.moveToNext()); } cursor.close(); } else { Log.d("测试", "数据库中没有"); cursor.close(); } return words; }
这是一个单例类,我采用了双重锁的方式,提高性能。
方法说明都在代码中有。
这个类中还有一个方法,用于获取Http访问的地址:
/** * 获取网络查找单词的对应地址 * * @param key 要查询的单词 * @return address 所查单词对应的http地址 */ public String getAddressForWords(final String key) { String address_p1 = "http://dict-co.iciba.com/api/dictionary.php?w="; String address_p2 = ""; String address_p3 = "&key=E568F04171398072F7EC5D8B4A6CBDB4"; if (isChinese(key)) { try { //此处非常重要!对中文的key进行重新编码,生成正确的网址 address_p2 = "_" + URLEncoder.encode(key, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } else { address_p2 = key; } return address_p1 + address_p2 + address_p3; }
看到代码中“此处非常重要!”的提示那段没,这也是一个卡了我几个小时的问题。
原本我是这样写的:
address_p2 = "_"+key;
问题是在查询中文的时候得不到任何数据,我还打印了访问的网址,Log出来的地址,我复制到ie浏览器返回有数据的,没有问题,又检查了WordsHandler,也没有问题。想了好久才意识到我在浏览器地址栏输入的中文会自动转码,而用HttpURLConnection访问时却不会自动转码。
所以在这里要手动的对中文进行重新编码。if里的isChinese()方法可以通过Unicode编码完美的判断中文汉字和符号
/** * 判断是否是中文 * * @param strName String类型的字符串 */ public static boolean isChinese(String strName) { char[] ch = strName.toCharArray(); for (int i = 0; i < ch.length; i++) { char c = ch[i]; if (isChinese(c)) { return true; } } return false; } /** * 根据Unicode编码完美的判断中文汉字和符号 * * @param c char类型的字符串 */ private static boolean isChinese(char c) { Character.UnicodeBlock ub = Character.UnicodeBlock.of(c); if (ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS || ub == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A || ub == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B || ub == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION || ub == Character.UnicodeBlock.HALFWIDTH_AND_FULLWIDTH_FORMS || ub == Character.UnicodeBlock.GENERAL_PUNCTUATION) { return true; } return false; }
到这里,基本的查词功能就能实现了!
其实我写这些代码的时候,会简单写一个Activity,里面有TextView,然后调用上述方法,测试我写的代码是否正确。
HttpUtil.sentHttpRequest(address, new HttpCallBackListener() { @Override public void onFinish(InputStream inputStream) { WordsHandler wordsHandler = new WordsHandler(); ParseXML.parse(wordsHandler, inputStream); words = wordsHandler.getWords(); wordsAction.saveWords(words); wordsAction.saveWordsMP3(words); } @Override public void onError() { } }); }
这就是测试的时候简单调用方法,看看能不能实现功能。
今天就到这里,明天继续后续内容!
更多相关文章
- SpringBoot 2.0 中 HikariCP 数据库连接池原理解析
- Android(安卓)让人又爱又恨的触摸机制(一)
- android之存储篇_SQLite数据库_让你彻底学会SQLite的使用
- 基于android的网络音乐播放器-本地音乐的加载和后台播放(一)
- Android拍照或从系统相册获取图片
- 备战面试旺季:三年开发经验,离开了某创业公司我用这些拿到了6个大
- 【Android】蓝牙开发——经典蓝牙:配对与解除配对 & 实现配对或连
- Android适配器进阶之三(抽象分类适配器)
- Qt on Android(安卓)实现App普通全屏、沉浸模式、粘性沉浸模式