继续接昨天的内容,没看过的可以点击 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

更多相关文章

  1. 【转】Android自适应不同分辨率或不同屏幕大小的layout布局(横屏
  2. Activity之间的相互调用与传递参数
  3. Android开发之旅:深入分析布局文件
  4. Android(安卓)简单动画中SurfaceView 的应用
  5. Android之封装好的异步网络请求框架
  6. Android(安卓)序列化对象接口Parcelable使用方法
  7. 调用Android其它Context的Activity
  8. Android加密(一)
  9. android 获取手机中所有的传感器Sensor类使用方法

随机推荐

  1. 自己编写的android汉字转拼音类(超全字库
  2. qt for android中文字体显示异常解决方案
  3. cocos2dx3.9 + android studio2.0 搭建项
  4. android 手机屏蔽广告 hosts
  5. Android : 高通平台Camera调试
  6. Android 在弹出Dialog(带EditText)的同时弹
  7. Android中几种常用图片加载库的使用
  8. Android(安卓)Service详解(一) 初识Service
  9. Android视图动画
  10. Android Top Camera Libraries 照相机API