原创内容,转载请注明出处
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相关内容。
运行效果如下
源代码如附件