前言:

自学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() {                }            }); }

这就是测试的时候简单调用方法,看看能不能实现功能。
今天就到这里,明天继续后续内容!

更多相关文章

  1. SpringBoot 2.0 中 HikariCP 数据库连接池原理解析
  2. Android(安卓)让人又爱又恨的触摸机制(一)
  3. android之存储篇_SQLite数据库_让你彻底学会SQLite的使用
  4. 基于android的网络音乐播放器-本地音乐的加载和后台播放(一)
  5. Android拍照或从系统相册获取图片
  6. 备战面试旺季:三年开发经验,离开了某创业公司我用这些拿到了6个大
  7. 【Android】蓝牙开发——经典蓝牙:配对与解除配对 & 实现配对或连
  8. Android适配器进阶之三(抽象分类适配器)
  9. Qt on Android(安卓)实现App普通全屏、沉浸模式、粘性沉浸模式

随机推荐

  1. Android(安卓)自定义UI圆角按钮
  2. View 控件EditText属性
  3. Say Hello To Android
  4. 在模拟器上运行编译好的android
  5. 《Android开发从零开始》——13.Table La
  6. 关于android中的内部存储与外部存储
  7. Android相对布局实现各种梅花效果
  8. Android 并发之Handler、Looper、Message
  9. 如何在Android上安装apk软件
  10. 5、frida进阶-Android逆向之旅---Hook神