android 项目练习:自己的词典app——生词本(二)
继续接昨天的内容,没看过的可以点击 android 项目练习:自己的词典app——生词本(一) 查看。
昨天已经把查词界面的功能代码都完成了,今天就来完成UI界面的设计,由于本身不具备太多的艺术细胞,和所花时间有限,UI界面仅仅是凸显功能,并不美观。
查词界面UI:
xml布局文件如下:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/searchWords_fatherLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#E5E6E0" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="com.example.app.vocabularybuilder.activity.MainActivity"> <SearchView android:id="@+id/searchWords_searchView" android:layout_width="match_parent" android:layout_height="wrap_content" android:queryHint="请输入要查询的单词" /> <LinearLayout android:id="@+id/searchWords_linerLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:visibility="invisible"> <RelativeLayout android:layout_width="match_parent" android:layout_height="60dp"> <TextView android:id="@+id/searchWords_key" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:layout_alignParentTop="true" android:layout_marginLeft="20dp" android:layout_marginStart="20dp" android:text="abc" android:textSize="40dp" /> </RelativeLayout> <LinearLayout android:id="@+id/searchWords_posE_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="20dp"> <ImageButton android:id="@+id/searchWords_voiceE" android:layout_width="25dp" android:layout_height="25dp" android:background="@android:color/transparent" android:src="@drawable/voice" /> <TextView android:id="@+id/searchWords_psE" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingTop="3dp" android:paddingLeft="5dp" android:paddingRight="5dp" android:paddingBottom="7dp" android:text="@string/psE" android:textColor="#3B3C3D" android:textSize="16dp" /> </LinearLayout> <LinearLayout android:id="@+id/searchWords_posA_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingLeft="40dp"> <ImageButton android:id="@+id/searchWords_voiceA" android:layout_width="25dp" android:layout_height="25dp" android:background="@android:color/transparent" android:src="@drawable/voice" /> <TextView android:id="@+id/searchWords_psA" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingTop="3dp" android:paddingLeft="5dp" android:paddingRight="5dp" android:paddingBottom="7dp" android:text="@string/psA" android:textColor="#3B3C3D" android:textSize="16dp" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:layout_width="30dp" android:layout_height="30dp" android:layout_gravity="center_vertical" android:src="@drawable/list" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:padding="10dp" android:text="@string/posAcceptation" android:textSize="20dp" /> </LinearLayout> <TextView android:id="@+id/searchWords_posAcceptation" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/layer_list_view" android:paddingBottom="5dp" android:paddingLeft="20dp" android:paddingRight="10dp" android:paddingTop="5dp" android:text="@string/pos" android:textSize="15dp" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:layout_width="30dp" android:layout_height="30dp" android:layout_gravity="center_vertical" android:src="@drawable/list" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:padding="10dp" android:text="@string/sent" android:textSize="20dp" /> </LinearLayout> </LinearLayout> <ScrollView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:background="@drawable/layer_list_view"> <TextView android:id="@+id/searchWords_sent" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingBottom="5dp" android:paddingLeft="20dp" android:paddingRight="10dp" android:paddingTop="5dp" android:text="@string/pos" android:textSize="15dp" /> </ScrollView> </LinearLayout></LinearLayout>
应该很好理解,都是最常用的控件,searchView在Activity中还设置了两个属性:
searchView = (SearchView) findViewById(R.id.searchWords_searchView);searchView.setSubmitButtonEnabled(true);//设置显示搜索按钮searchView.setIconifiedByDefault(false);//设置不自动缩小为图标
然后看下效果图吧
xml布局文件的代码里可以看到最后一个例句的TextView外面包了一个ScrollView,因为上面其他控件的大小都是固定,只有这个TextView是补充剩余屏幕控件的,如果例句内容较多的话会出现超出屏幕,因此这里加了ScrolView保证能显示完整的例句内容。
还有一个细节问题,就是点击SearchView是会弹出软键盘,但是默认不会自动收回,所以我通过重写一个OnClickListener实现点击SwearchView以外的地方实现收回软键盘。
searchWords_fatherLayout = (LinearLayout) findViewById(R.id.searchWords_fatherLayout);searchWords_fatherLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //点击输入框外实现软键盘隐藏 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(v.getWindowToken(), 0); } });
这里的searchWords_fatherLayout就是UI布局最外层的LinerLayout了。
这是我是参考了其他大神的方法: Android点击EditText文本框之外任何地方隐藏键盘的解决办法 。
还有一个细节点,当手机由竖直变为水平是,Activity也会跟着变换,这样会导致显示不全,因此我设置Activity永远是竖直状态的,方法是在Manifest.xml文件中添加:
<activity android:name=".activity.MainActivity" android:screenOrientation="portrait"> <!-- 固定竖屏 --> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
细心的朋友可能还看到我添加了ActionBar,那是打算做一些导航按钮和添加生词本的按钮,预先留出位置,目前还没有实现。
到此基本没有什么明显的瑕疵了,也实现显示查词结果的内容。
接下来就是发音MP3的储存和播放问题了。
查词发音
昨天我们访问查词接口的时候,还记得返回了这个http://res.iciba.com/resource/amp3/0/0/34/d1/34d1f91fb2e514b8576fab1a75a89a6b.mp3 ,这是MP3的地址,通过Http访问就可以得到对应的MP3文件,我们要做的就是把这个文件保存手机的SD卡中,并也有能够播放这个文件的方法。为什么是放在SD卡中?当然是为了保护手机的性能吗,这和在电脑上下载文件一般不放在C盘,而是放在D、E、F等盘中是一个道理。
现在util包下新建一个FileUtil类,用于封装对SD卡存储文件操作:
public class FileUtil { /** * SD卡的目录 */ private String SDPath; /** * 本app存储的目录 */ private String AppPath; /** * 本类的单例 */ private static FileUtil fileUtil; /** * 私有化的构造器 */ private FileUtil() { //如果手机已插入SD卡,且应用程序具有读写SD卡的功能,则返回true if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { SDPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/"; //清理测试时产生的文件// File f = new File(SDPath + "VocabularyBuilder");// deleteFile(f); File fileV = createSDDir(SDPath, "VocabularyBuilder"); AppPath = fileV.getAbsolutePath() + "/"; } } /** * 单例类FileUtil获取实例方法 */ public static FileUtil getInstance() { if (fileUtil == null) { synchronized (FileUtil.class) { if (fileUtil == null) { fileUtil = new FileUtil(); } } } return fileUtil; } /** * 创建目录 * * @param path 文件夹的路径 * @param dirName 文件夹名 */ public File createSDDir(String path, String dirName) { File dir = new File(path + dirName); if (dir.exists() && dir.isDirectory()) { return dir; } dir.mkdir(); Log.d("测试", "创建目录成功"); return dir; } /** * 创建SD文件 * * @param path 文件的路径 * @param fileName 文件名 */ public File createSDFile(String path, String fileName) { File file = new File(path + fileName); if (file.exists() && file.isFile()) { return file; } try { file.createNewFile(); Log.d("测试", "创建文件成功"); } catch (IOException e) { e.printStackTrace(); } return file; } /** * 向SD卡中写入文件 * * @param path 文件夹名 * @param fileName 文件名 * @param inputStream 输入流 */ public void writeToSD(String path, String fileName, InputStream inputStream) { OutputStream outputStream = null; try { File dir = createSDDir(AppPath, path); File file = createSDFile(dir.getAbsolutePath() + "/", fileName); outputStream = new FileOutputStream(file); int length; byte[] buffer = new byte[2 * 1024]; while ((length = inputStream.read(buffer)) != -1) { //注意这里的length; //利用read返回的实际成功读取的字节数,将buffer写入文件, // 否则将会出现错误的字节,导致保存文件与源文件不一致 outputStream.write(buffer, 0, length); } outputStream.flush(); Log.d("测试", "写入成功"); } catch (IOException e) { e.printStackTrace(); Log.d("测试", "写入失败"); } finally { try { if (outputStream != null) { outputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } } /** * 获取文件在SD卡上绝对路径,如无该文件返回"" * * @param fileName 单词对应的文件夹名 */ public String getPathInSD(String fileName) { File file = new File(AppPath + fileName); if (file.exists()) { return file.getAbsolutePath(); } return ""; }}
里面已经说得比较详细了,代码中”//注意这里的length;“这段内容最重要,一开始我是用:
outputStream.write(buffer);
这个方法写入的,但是测试的时候发现,音频会出现问题,查看文件大小会发现和源文件大小不一样。实际上这种方式写入会存在重复写入的问题,导致最终保存MP3比原文件要打,比方的时候会出现一小段重复,或者干脆不能播放的问题。
因此,这里要调用:outputStream.write(buffer, 0, length);
这个方法,此方法会利用read返回的实际成功读取的字节数,将buffer写入文件,避免重复写入同一个字符。
除了上面说到的,还有一个注意的点,这个问题同样困扰了我几个小时(⊙o⊙)…
毕竟是自己自学,常常会被一些小问题卡住好久 = _ =‘’
就总结说下,File创建一个文件如果是在很多层级下的话,一定要一层一层地创建,举个例子,我要创建一个文件的绝对路径是:E:/Directory/subDirectory/file.txt
但在创建之前”E:“下面没有”Directory“这个文件夹,如果这时候直接调用
File file = new File(E:/Directory/subDirectory/file.txt);file.createNewFile();
是没有办法成功创建的,因此只能先创建”Directory“这个文件夹,再创建”subDirectory“这个文件夹,最后创建”file.txt“这个文件才行。
所以上面这个FileUtil类中的writeToSD()方法里,我是先创建文件夹,在创建文件的。
同样的,删除文件夹的时候,如果里面有文件,也是不能删除的,只有先把里面的文件删除光,才能删除文件夹。具体方法可以参考这个递归删除文件夹的方法:
/** * 递归删除文件夹 * * @param file 文件夹或者文件名 */ private void deleteFile(File file) { if (file.exists()) {//判断文件是否存在 if (file.isFile()) {//判断是否是文件 file.delete();//删除文件 } else if (file.isDirectory()) {//否则如果它是一个目录 File[] files = file.listFiles();//声明目录下所有的文件 files[]; for (int i = 0; i < files.length; i++) {//遍历目录下所有的文件 deleteFile(files[i]);//把每个文件用这个方法进行迭代 } file.delete();//删除文件夹 } Log.d("测试", "删除成功"); } }
完成了这个类,我们回到昨天创建的WordsAciton类中,添加这样两个方法:
/** * 保存words的发音MP3文件到SD卡 * 先请求Http,成功后保存 * * @param words words实例 */ public void saveWordsMP3(Words words) { String addressE = words.getPronE(); String addressA = words.getPronA(); if (addressE != "") {//判断有地址才发送网络请求 final String filePathE = words.getKey(); HttpUtil.sentHttpRequest(addressE, new HttpCallBackListener() { //请求完成后的回调方法 @Override public void onFinish(InputStream inputStream) { //调用FileUtil的方法将MP3文件保存到SD卡 FileUtil.getInstance().writeToSD(filePathE, "E.mp3", inputStream); } @Override public void onError() { } }); } if (addressA != "") { final String filePathA = words.getKey(); HttpUtil.sentHttpRequest(addressA, new HttpCallBackListener() { @Override public void onFinish(InputStream inputStream) { FileUtil.getInstance().writeToSD(filePathA, "A.mp3", inputStream); } @Override public void onError() { } }); } } /** * 播放words的发音 * * @param wordsKey 单词的key * @param ps E 代表英式发音 * A 代表美式发音 * @param context 上下文 */ public void playMP3(String wordsKey, String ps, Context context) { String fileName = wordsKey + "/" + ps + ".mp3"; String adrs = FileUtil.getInstance().getPathInSD(fileName); if (player != null) { if (player.isPlaying()) { player.stop(); } player.release(); player = null; } if (adrs != "") {//有内容则播放 player = MediaPlayer.create(context, Uri.parse(adrs)); Log.d("测试", "播放"); player.start(); } else {//没有内容则重新去下载 Words words = getWordsFromSQLite(wordsKey); saveWordsMP3(words); } }
第一个方法就是网络请求,没什么问题。
第二个是meadiaPlayer播放MP3文件,加入了一些判断,如果没有找到音频文件的一些补救措施——再去下载一次。
最后,在Acitivity中调用这两个方法,完整的Activity代码如下:
public class MainActivity extends Activity { private SearchView searchView; private TextView searchWords_key, searchWords_psE, searchWords_psA, searchWords_posAcceptation, searchWords_sent; private ImageButton searchWords_voiceE, searchWords_voiceA; private LinearLayout searchWords_posA_layout,searchWords_posE_layout, searchWords_linerLayout, searchWords_fatherLayout; private WordsAction wordsAction; private Words words = new Words(); /** * 网络查词完成后回调handleMessage方法 */ private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 111: //判断网络查找不到该词的情况 if (words.getSent().length() > 0) { upDateView(); } else { searchWords_linerLayout.setVisibility(View.GONE); Toast.makeText(MainActivity.this, "抱歉!找不到该词!", Toast.LENGTH_SHORT).show(); } Log.d("测试", "tv保存2"); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); wordsAction = WordsAction.getInstance(this); //初始化控件 searchWords_linerLayout = (LinearLayout) findViewById(R.id.searchWords_linerLayout); searchWords_posA_layout = (LinearLayout) findViewById(R.id.searchWords_posA_layout); searchWords_posE_layout = (LinearLayout) findViewById(R.id.searchWords_posE_layout); searchWords_fatherLayout = (LinearLayout) findViewById(R.id.searchWords_fatherLayout); searchWords_fatherLayout.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //点击输入框外实现软键盘隐藏 InputMethodManager inputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); inputMethodManager.hideSoftInputFromWindow(v.getWindowToken(), 0); } }); searchWords_key = (TextView) findViewById(R.id.searchWords_key); searchWords_psE = (TextView) findViewById(R.id.searchWords_psE); searchWords_psA = (TextView) findViewById(R.id.searchWords_psA); searchWords_posAcceptation = (TextView) findViewById(R.id.searchWords_posAcceptation); searchWords_sent = (TextView) findViewById(R.id.searchWords_sent); searchWords_voiceE = (ImageButton) findViewById(R.id.searchWords_voiceE); searchWords_voiceE.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { wordsAction.playMP3(words.getKey(), "E", MainActivity.this); } }); searchWords_voiceA = (ImageButton) findViewById(R.id.searchWords_voiceA); searchWords_voiceA.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { wordsAction.playMP3(words.getKey(), "A", MainActivity.this); } }); searchView = (SearchView) findViewById(R.id.searchWords_searchView); searchView.setSubmitButtonEnabled(true);//设置显示搜索按钮 searchView.setIconifiedByDefault(false);//设置不自动缩小为图标 searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { loadWords(query); return true; } @Override public boolean onQueryTextChange(String newText) { return false; } }); } /** * 读取words的方法,优先从数据中搜索,没有在通过网络搜索 */ public void loadWords(String key) { words = wordsAction.getWordsFromSQLite(key); if ("" == words.getKey()) { String address = wordsAction.getAddressForWords(key); 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); handler.sendEmptyMessage(111); } @Override public void onError() { } }); } else { upDateView(); } } /** * 更新UI显示 */ public void upDateView() { if (words.getIsChinese()) { searchWords_posAcceptation.setText(words.getFy()); searchWords_posA_layout.setVisibility(View.GONE); searchWords_posE_layout.setVisibility(View.GONE); } else { searchWords_posAcceptation.setText(words.getPosAcceptation()); if(words.getPsE()!="") { searchWords_psE.setText(String.format(getResources().getString(R.string.psE), words.getPsE())); searchWords_posE_layout.setVisibility(View.VISIBLE); }else { searchWords_posE_layout.setVisibility(View.GONE); } if(words.getPsA()!="") { searchWords_psA.setText(String.format(getResources().getString(R.string.psA), words.getPsA())); searchWords_posA_layout.setVisibility(View.VISIBLE); }else { searchWords_posA_layout.setVisibility(View.GONE); } } searchWords_key.setText(words.getKey()); searchWords_sent.setText(words.getSent()); searchWords_linerLayout.setVisibility(View.VISIBLE); } //加载actionbar的菜单 @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.actionbar_layout_menu, menu); return super.onCreateOptionsMenu(menu); }}
到这里整个查词的功能就完成了!谢谢大家的观看!
后续我完成了生词本的功能还会贴出来的!
这块功能的源码我上传了GitHub
有兴趣的可以下载看看:https://github.com/huburt-Hu/VocabularyBuilder.git
更多相关文章
- 【转】Android自适应不同分辨率或不同屏幕大小的layout布局(横屏
- Activity之间的相互调用与传递参数
- Android开发之旅:深入分析布局文件
- Android(安卓)简单动画中SurfaceView 的应用
- Android之封装好的异步网络请求框架
- Android(安卓)序列化对象接口Parcelable使用方法
- 调用Android其它Context的Activity
- Android加密(一)
- android 获取手机中所有的传感器Sensor类使用方法