原创内容,转载请注明出处
1、介绍
学习Android已经有一段时间了,但是都是一些零零散散的知识点,还需要能够将这些知识点串起来,以便加深对Android的了解。下面将通过一个小项目来将最近所学的知识串起来,在该项目中会涉及到Activity、Service、BroadCast Recevier三大组件;还有ListActivity、TabActivity;使用Android的MediaPlayer类播放音频文件;Android的多线程类HandlerThread和Handler处理类使用;Java文件下载、输入输出流的使用;SAX基于事件驱动解析XML文件;Properties文件的处理;Tomcat服务的简单搭建等知识点。
2、分析
首先该Mp3播放器的主要功能如下:Mp3文件播放暂停,Mp3文件下载,本地Mp3文件展示,服务器Mp3文件展示。首先用户进入Mp3播放器,查看目前服务器上有哪些Mp3歌曲,将需要的Mp3文件下载下来,然后在本地文件列表中播放Mp3歌曲。
功能分析
1、在该项目中主要有四个界面:服务器Mp3歌曲文件列表,本地歌曲文件列表,设置界面,关于界面。
2、服务器歌曲文件列表页面,包含获取服务器Mp3文件列表、下载服务器Mp3文件、列表更新。
3、本地歌曲列表,包含本地Mp3文件展示、歌曲文件播放暂停等操作、列表更新。
4、设置界面,提供系统的通用设置给用户,像本地文件路径等。
5、关于界面,展示系统介绍。
6、考虑到操作方便,将服务器文件列表和本地文件列表合并在一个切换标签页中展示。
3、项目主要说明
3.1、使用Tomcat搭建mp3 Web工程
1.在tomcat目录下的webapps中新建文件夹mp3,即创建mp3简单应用工程。
2.在mp3下创建WEB-INF文件夹,并在WEB-INF文件夹下创建web.xml文件,此时完成mp3 web工程的搭建。
<?xml version="1.0" encoding="ISO-8859-1"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> </web-app>
3.进入doc命令,启动tomcat服务。
4.在mp3目录下添加mp3歌曲和配置文件resources.xml。resources.xml文件代表服务器上所有文件列表信息,由Android客户端来获取
<?xml version="1.0" encoding="utf-8"?> <resources> <resource> <id>00001</id> <mp3Name>chunni.mp3</mp3Name> <mp3Size>4122018</mp3Size> <mp3Path>http://192.168.2.115:8080/mp3</mp3Path> </resource> <resource> <id>00002</id> <mp3Name>17sui.mp3</mp3Name> <mp3Size>3856845</mp3Size> <mp3Path>http://192.168.2.115:8080/mp3</mp3Path> </resource> <resource> <id>00003</id> <mp3Name>jintian.mp3</mp3Name> <mp3Size>3623184</mp3Size> <mp3Path>http://192.168.2.115:8080/mp3</mp3Path> </resource> </resources>
3.2、构建服务器文件列表页面
1.使用eclipse开发工具创建Mp3RemoteActivity类和对应布局文件activity_mp3_remote.xml。由于使用ListView控件,这里我使用自定义的ListView内容布局list.xml。
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="horizontal" > <TextView android:id="@+id/mp3Name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" /> <TextView android:id="@+id/mp3Size" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" /> <TextView android:id="@+id/mp3Path" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="invisible" /> </RelativeLayout>
2.修改activity_mp3_remote.xml布局文件,增加ListView控件,id值设为系统自带andorid.R.id.list。
<ListView android:id="@android:id/list" android:layout_width="fill_parent" android:layout_height="wrap_content" />
3.修改Mp3RemoteActivity类,让它去继承ListActivity类。重写onCreate方法,首先在该方法中注册自定义下载广播接收器,然后在该读取服务器文件列表,并更新ListView控件的数据。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_mp3_remote); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(Mp3Contant.downloadaction); //注册下载文件广播接收 registerReceiver(new DownloadReceiver(), intentFilter); //更新ListView数据 update(); }
4.更新MP3列表使用多线程去获取服务器上的数据,因为该过程可能会由于网络等原因导致Activity无响应,影响用户的体验度。
/** * 更新MP3列表 * * @author Alan * @time 2015-7-21 下午3:53:24 */ private void update(){ //创建线程 HandlerThread handlerThread = new HandlerThread("updateUI"); handlerThread.start(); //创建Handler Handler handler = new Handler(handlerThread.getLooper()); //将Runnable加到Handler队列中 handler.post(runnable); }
获取服务器文件列表后,并更新ListView控件的数据。由于在Android的UI是线程不安全,故而在Android4.0版本及以上规定,更新UI不能通过其他线程来更新,只能调用主线程更新UI。对于更新UI的问题,Android提供了多种处理方式,第一Activity类中有一个runOnUiThread()方法,该方法中的内容将在UI线程执行,故而可将UI更新的方法放置在该方法中;第二种是使用多线程HandlerThread和Handler处理类的回调函数处理;第三种是通过广播机制也可实现,目前只了解过这三种方式,亲测都ok,本案例将使用runOnUiThread方法来完成。
@Override public void run() { //获取下载内容 String content = DownloadUtil.readFile(Mp3Contant.serverDefaultMp3Path,Mp3Contant.serverDefaultResourceName); //将下载内容包装成InputSource对象 InputSource inputSource = new InputSource(new StringReader(content)); //解析MP3内容列表 List<Mp3> mp3s = XParse.parse(inputSource); //创建ListAdapter适配器 adapter = convertListAdapterByMp3s(mp3s); //更新UI处理必须在UI线程上 runOnUiThread(new Runnable() { @Override public void run() { // 更新ListView的ListAdapter适配器 Mp3RemoteActivity.this.setListAdapter(adapter); } }); } };
还有下载文件代码和解析Xml文件的代码未列出这些都属于Java Se部分的内容,具体可看源代码。
5.重写onListItemClick方法(listview点击事件),在该方法中使用多线程来下载服务器上的mp3文件。
/** * listview点击事件 * @param l * @param v * @param position * @param id * @author Alan * @time 2015-7-21 下午4:11:54 */ @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); HashMap<String, String> hashMap = (HashMap<String, String>) l.getItemAtPosition(position); String mp3Name = hashMap.get("mp3Name");//获取MP3名称 String mp3Path = hashMap.get("mp3Path");//获取MP3路径 //Mp3下载采用多线程进行 HandlerThread thread = new HandlerThread("downloadFile"); thread.start(); Handler handler = new Handler(thread.getLooper()); //开始处理Mp3文件下载 handler.post(new MyRunnable(mp3Path, mp3Name)); }
下载文件成功后,通过Android的广播机制,发布广播,在页面上提示下载成功。
class MyRunnable implements Runnable{ private String mp3Name; private String mp3Path; public MyRunnable(String mp3Path,String mp3Name){ this.mp3Path = mp3Path; this.mp3Name = mp3Name; } @Override public void run() { //下载文件 DownloadUtil.downloadFile(mp3Path, mp3Name,Mp3Contant.defaultMp3Path,mp3Name); //发送广播 Intent intent = new Intent(); intent.setAction(Mp3Contant.downloadaction); intent.putExtra("msg", mp3Name+"下载成功"); sendBroadcast(intent); } }
以下是广播接收器的代码,主要是提示最终下载结果的消息。
@Override public void onReceive(Context context, Intent intent) { Bundle bundle = intent.getExtras(); String msg = (String) bundle.get("msg"); //提示消息 Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); }
6.重写onCreateOptionsMenu方法去添加菜单按钮,并重写onOptionsItemSelected方法去处理菜单按钮的具体监听事件。
@Override public boolean onCreateOptionsMenu(Menu menu) { //添加设置按钮 menu.add(1,SETTING,1,R.string.setting); //添加关于按钮 menu.add(1,ABOUT,2,R.string.about); //添加更新按钮 menu.add(1,UPDATE,3,R.string.update); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == UPDATE) { //更新列表 update(); }else if(id == SETTING){ //进入设置界面 Intent intent = new Intent(); intent.setClass(this, SettingActivity.class); startActivity(intent); }else if(id == ABOUT){ //进入关于界面 Intent intent = new Intent(); intent.setClass(this, AboutActivity.class); startActivity(intent); } return super.onOptionsItemSelected(item); }
3.3、构建本地文件列表展示页面
1.创建Mp3LocalActivity类和对应的布局文件activity_mp3_local.xml。
2.修改布局文件内容,添加ListView控件,并添加播放、上一首、下一首三个按钮,具体布局如下
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" 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.cygoat.mp3.activity.Mp3LocalActivity" > <LinearLayout android:id="@+id/linearlayout" android:layout_width="fill_parent" android:layout_height="wrap_content" > <ListView android:id="@android:id/list" android:layout_width="fill_parent" android:layout_height="wrap_content" /> </LinearLayout> <RelativeLayout android:layout_width="fill_parent" android:layout_height="wrap_content"> <Button android:id="@+id/prev" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/prev" /> <Button android:id="@+id/play" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="@string/start"/> <Button android:id="@+id/next" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:text="@string/next" /> </RelativeLayout> </LinearLayout>
3.修改Mp3LocalActivity类,重写onCreate方法,在该方法中为按钮注册监听器,并更新ListView数据。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_mp3_local); prev = (Button) findViewById(R.id.prev); play = (Button) findViewById(R.id.play); next = (Button) findViewById(R.id.next); //添加监听器 prev.setOnClickListener(this); play.setOnClickListener(this); next.setOnClickListener(this); //更新ListView数据 update(); }
更新ListView数据主要从本地Mp3目录中搜索mp3文件,然后展示。
/** * 更新MP3列表 * * @author Alan * @time 2015-7-21 下午3:53:24 */ private void update(){ //获取Properties对象 Properties properties = PropertiesUtil.getProperties(Mp3Contant.settingFileName); //获取MP3路径 String mp3Path = properties.getProperty(Mp3Contant.CharContant.mp3Path); //获取mp3文件 File[] files = getMp3Files(mp3Path); //将MP3文件转换ListAdapter ListAdapter adapter = convertListAdapterByFiles(files); //为Activity设置adapter this.setListAdapter(adapter); }
4.考虑到当从播放器回到主页时,播放器的Activity处于不可见状态,该Activity可能会被Java垃圾回收器回收(在Android优先级顺序由高到低:处于可见的Activity>Service>不可见Activity)。因此为了避免播放音乐资源被垃圾回收器回收,导致播放中断,故而使用Service组件来完成音乐播放。
音乐播放操作事件主要在点击播放、上一首、下一首按钮,和点击ListView中的数据项触发。
重写onListItemClick方法,在该方法中实现音乐播放的操作。代码如下
/** * listview点击事件 * @param l * @param v * @param position * @param id * @author Alan * @time 2015-7-21 下午4:11:54 */ @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); //创建跳转Intent实例 Intent intent = new Intent(); //将文件名称设置到Intent实例中 intent.putExtra("fileName", getFullFileName(position)); if(currPosition != position){ intent.putExtra("isNew", true); }else{ intent.putExtra("isNew", false); } intent.setClass(this, PlayService.class); //开启mp3播放服务 startService(intent); //设置当前播放mp3歌曲位置 currPosition = position; }
5.创建播放音乐服务PlayService类,在该类中实现音乐播放功能处理。处理音乐播放主要使用MediaPlayer类,由于该类使用比较简单,具体查看源代码。PlayService代码如下
package com.cygoat.mp3.service; import android.app.Service; import android.content.Intent; import android.media.MediaPlayer; import android.os.Bundle; import android.os.IBinder; public class PlayService extends Service { private MediaPlayer mediaPlayer; //当前播放状态 private static final int INIT_STATUS = 0;//初始化状态 private static final int ACTIVE_STATUS = 1;//播放状态 private static final int PAUSE_STATUS = 2;//暂停状态 private static final int STOP_STATUS = 3;//停止状态 //播放命令 public static final int START_COMMAND = 1;//启动命令 public static final int PAUSE_COMMAND = 2;//暂停命令 public static final int STOP_COMMAND = 3;//停止命令 private int playStatus = INIT_STATUS; @Override public IBinder onBind(Intent intent) { return null; } @Override public void onCreate() { super.onCreate(); mediaPlayer = new MediaPlayer(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Bundle bundle = intent.getExtras(); //取出文件名称变量和是否是切换歌曲播放变量 String fileName = (String) bundle.get("fileName"); boolean isNew = (Boolean) bundle.get("isNew"); Integer command = (Integer) bundle.get("command"); //播放 play(fileName, isNew,command); return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { super.onDestroy(); } private void play(String fileName , boolean isNew , Integer command){ //如果是切换新的歌曲,则直接播放 if(isNew){ active(fileName); return; } //同一首歌之间状态切换 if((playStatus == INIT_STATUS || playStatus == STOP_STATUS) && ((command==null?START_COMMAND:command) ==START_COMMAND)){ //当前歌曲是初始化和停止状态,如果此时下发播放命令,则开始播放 active(fileName); return; } if(playStatus == ACTIVE_STATUS && ((command==null?PAUSE_COMMAND:command)==PAUSE_COMMAND)){ //当前歌曲是播放状态,如果此时下发暂停命令,则暂停播放 pause(); return; } if(playStatus == PAUSE_STATUS && ((command==null?START_COMMAND:command)==START_COMMAND)){ //当前歌曲是暂停状态,如果此时下发播放命令,则继续播放 reActive(); return; } if((playStatus == ACTIVE_STATUS || playStatus == PAUSE_STATUS) && ((command==null?STOP_COMMAND:command) ==STOP_COMMAND)){ //当前歌曲状态是播放和暂停,如果此时下发停止命令,则停止播放 stop(); return; } } /** * 播放歌曲 * @param fileName * @author Alan * @time 2015-7-21 下午7:31:24 */ private void active(String fileName){ try{ mediaPlayer.reset(); mediaPlayer.setDataSource(fileName); mediaPlayer.prepare(); mediaPlayer.start(); playStatus = ACTIVE_STATUS; }catch(Exception e){ e.printStackTrace(); } } /** * 暂停播放 * * @author Alan * @time 2015-7-21 下午7:31:37 */ private void pause(){ mediaPlayer.pause(); playStatus = PAUSE_STATUS; } /** * 继续播放 * * @author Alan * @time 2015-7-21 下午7:31:49 */ private void reActive(){ mediaPlayer.start(); playStatus = ACTIVE_STATUS; } /** * 停止播放 * * @author Alan * @time 2015-7-21 下午8:20:49 */ private void stop(){ mediaPlayer.stop(); playStatus = STOP_STATUS; } }
6.重写onCreateOptionsMenu方法去添加菜单按钮,并重写onOptionsItemSelected方法去处理菜单按钮的具体监听事件。
@Override public boolean onCreateOptionsMenu(Menu menu) { //添加设置按钮 menu.add(1,SETTING,1,R.string.setting); //添加关于按钮 menu.add(1,ABOUT,2,R.string.about); //添加更新按钮 menu.add(1,UPDATE,3,R.string.update); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == UPDATE) { //更新列表 update(); }else if(id == SETTING){ //进入设置界面 Intent intent = new Intent(); intent.setClass(this, SettingActivity.class); startActivity(intent); }else if(id == ABOUT){ //进入关于界面 Intent intent = new Intent(); intent.setClass(this, AboutActivity.class); startActivity(intent); } return super.onOptionsItemSelected(item); }
3.4、构建切换标签主界面
1.创建MainActivity和对应布局文件activity_main.xml。
2.修改activity_main.xml布局文件,由于是使用TabActivity,该文件必须以TabHost作为根节点,根节点下必须包括TabWidget和FrameLayout。
<?xml version="1.0" encoding="utf-8"?> <TabHost xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/tabhost" android:layout_width="fill_parent" android:layout_height="fill_parent" > <LinearLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical" android:padding="5dp" > <TabWidget android:id="@android:id/tabs" android:layout_width="fill_parent" android:layout_height="wrap_content" /> <FrameLayout android:id="@android:id/tabcontent" android:layout_width="fill_parent" android:layout_height="fill_parent" android:padding="5dp" /> </LinearLayout> </TabHost>
3.修改MainActivity继承TabActivity 。重写onCreate方法中将Mp3RemoteActivity和Mp3LocalActivity界面添加到主界面上的Tab切换页签上。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //获取tabHost,即整个Tab TabHost tabHost = getTabHost(); //创建Intent跳转对象 Intent intent = new Intent(); intent.setClass(this, Mp3LocalActivity.class); //新建一个标签页 TabHost.TabSpec spec = tabHost.newTabSpec("mp3LocalActivity"); //设置标签名称,还可设置图片 spec.setIndicator("本地"); //设置Intent跳转对象 spec.setContent(intent); //添加标签页 tabHost.addTab(spec); //创建Intent跳转对象 intent = new Intent(); intent.setClass(this, Mp3RemoteActivity.class); //新建一个标签页 spec = tabHost.newTabSpec("mp3RemoteActivity"); //设置标签名称,还可设置图片 spec.setIndicator("远程"); //设置Intent跳转对象 spec.setContent(intent); //添加标签页 tabHost.addTab(spec); }
3.4、构建设置界面
1.创建SettingActivity和对应布局文件activity_setting。
2.修改布局文件,在布局文件中主要添加Mp3文件目录的文本框。
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="${relativePackage}.${activityClass}" > <TextView android:id="@+id/mp3PathTextview" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/mp3Path" /> <EditText android:id="@+id/mp3Path" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/mp3PathTextview" android:hint="@string/mp3Path" /> <Button android:id="@+id/save" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:text="@string/save" /> </RelativeLayout>
3.修改SettingActivity文件,在该类中主要是获取配置文件信息和保存设置信息到配置文件中。
package com.cygoat.mp3.activity; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.util.Properties; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; import com.cygoat.mp3.Mp3Contant; import com.cygoat.mp3.R; import com.cygoat.util.IOUtil; public class SettingActivity extends Activity { private EditText mp3PathText; private Button save; private Properties properties = new Properties(); private static final String MP3_PATH = "mp3Path"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_setting); mp3PathText = (EditText) findViewById(R.id.mp3Path); save = (Button) findViewById(R.id.save); getSetting(); showSetting(); save.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { File file = new File(Mp3Contant.settingFileName); FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream(file); properties.put(MP3_PATH, mp3PathText.getText().toString()); properties.store(fileOutputStream, "mp3Path"); Toast.makeText(SettingActivity.this, "保存成功", Toast.LENGTH_SHORT).show(); Intent intent = new Intent(); intent.setClass(SettingActivity.this, MainActivity.class); startActivity(intent); } catch (Exception e) { e.printStackTrace(); } finally { IOUtil.close(fileOutputStream); } } }); } private void showSetting(){ mp3PathText.setText(properties.getProperty(MP3_PATH)); } private void getSetting() { File file = new File(Mp3Contant.settingFileName); FileInputStream fileInputStream = null; try { if (!file.exists()) { //文件不存在则创建文件 file.createNewFile(); } //构建文件输入流 fileInputStream = new FileInputStream(file); //将文件流装载进Properties对象中 properties.load(fileInputStream); } catch (Exception e) { e.printStackTrace(); } finally { IOUtil.close(fileInputStream); } } }
3.5、构建关于界面
创建AboutActivity和对应布局文件activity_about.xml。由于该类比较简单,详情可见源代码。
以上可能有Java部分的源代码没有去说明,像xml文件解析、远程文件下载、Properties文件解析和保存等。这些代码都属于Java部分,有兴趣的读者查看Java相关内容。
运行效果如下
源代码如附件