文字与音频的组合交互无论是PC端还是移动端都是一个传统的、经典的交互方式,唤醒的是人的视觉与听觉能力与感受。应用场景也特别多,比如有声小说,歌曲与歌词等。而自己为解决个人生活中的一个问题而开发的这个简单的APP ,也算是文字与音频的典型应用场景。
一直以来对自己的普通话不标准而感到烦恼,最近终于下定决心要好好的练习普通话,于是找来了很多文章和配套音频,但文章和音频是分开的,练习的时候的先打开文章,然后再通过音频播放器播放配套音频。即使在音乐播放器中播放音频,但因为文章也不是歌词形式,也做不到我想要的同步。于是自己开发了一个简单的APP,以满足自己打开APP后边看文章,边听文章朗读音频。至于测试自己普通话是否有进步等需求,可能还需要开发录音与比对等功能,那就是后话了。
交互效果:
技术实现:
实现这个效果大致可分成两块,一是文章的显示,二是配套音频的播放控制。为此需要先准备好文章文件和音频文件。需要注意的是我的文章是通过*.txt文本文件存放且存放的编码格式是utf-8(另存为中可以选择对应的编码格式),不注意这个问题可能获取到的文章在项目中会出现乱码问题。音频文件一定要是Android系统支持的格式,比如*.mp3、*.m4a等。
1、文章显示
(1)文章的存放与内容获取问题
文章可存放到项目中的assets资产目录中,也可以作为资源存放到res/raw目录下,不同的存放方式使得获取文章数据的方式有些不同。具体介绍如下:
a)存放到assets目录:
AssetManager am = getAssets();//获取资产管理器类
InputStream is = am.open("circle.txt");//打开指定文章名的输入流,其中circle.txt是文章名,需要带后缀名。
b)存放到res/raw目录:
//获取资源管理器,然后打开指定资源id的输入流,其中R.raw.circle是文章的资源id。
InputStream is =getResources().openRawResource(R.raw.circle);
说明:这两种方式各有优缺点而一但获取到对应的输入流,之后的操作就是JavaIO处理了。
(2)文章数据的显示
文章数据的显示可以使用TextView控件,但TextView要显示的数据需要是字符串的,所以需要把输入流转化成字符串才好再通过TextView的setText()方法显示到TextView控件上。并且由于文章的显示可能超过一屏,默认情况下一屏之后的内容会看不到,所以最好使用ScrollView包裹一下TextView,使其具有滑动功能,以方便用户查看一屏之后的内容,具体布局与逻辑实现代码如下:
a)布局实现部分:
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/rl_title"
android:scrollbars="none">
<TextView
android:id="@+id/tv_circle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:lineSpacingExtra="2dp"
android:lineSpacingMultiplier="1.2"
android:textColor="#000"
android:textSize="20sp"/>
ScrollView>
b)逻辑实现部分:
try {
AssetManageram = getAssets();
InputStream is = am.open("circle.txt");
InputStreamReader isr = newInputStreamReader(is);
BufferedReader br = newBufferedReader(isr);
String str = null;
StringBuffer sb = newStringBuffer();
while ((str = br.readLine())!= null) {
sb.append(str);
sb.append("\n");
}
mCircleTv.setText(sb.toString());
} catch(IOException e) {
e.printStackTrace();
}
因为存放文章文件的方式不同,所以获取文章输入流的方式也不同,但之后的部分是一样的,我们可以把相同的部分代码进行重构,使代码变的优雅,实现代码如下:
/**
* 获取文章字符串数据
* @param is
*/
privateString getCircleContentStr(InputStream is) {
try {
InputStreamReader isr = newInputStreamReader(is);
BufferedReader br = newBufferedReader(isr);
String str = null;
StringBuffer sb = newStringBuffer();
while ((str = br.readLine())!= null) {
sb.append(str);
sb.append("\n");
}
return sb.toString();
} catch(IOException e) {
e.printStackTrace();
}
return null;
}
mCircleTv = (TextView) findViewById(R.id.tv_circle);//获取显示文章的控件TextView
AssetManager am =getAssets();
try {
//这是把文章文本文件当raw类型数据时获取输入流的方式
//InputStream is = getResources().openRawResource(R.raw.circle);
InputStream is = am.open("circle.txt");//这是把文章文本文件当资产时获取输入流的方式
String circleContentStr =getCircleContentStr(is);//把输入流转化成字符串的方法
mCircleTv.setText(circleContentStr);//文章数据的显示
} catch(IOException e1) {
e1.printStackTrace();
}
2、音频控制
对于音频控制在Android里通常使用MediaPlayer类,这个类可以播放音频,暂停音频,停止音频,释放音频资源,也可以重置音频,监听音频是否播放完成等,通常如果业务比较复杂的时候,我们应该把对音频的控制逻辑放到专门的Service组件里去,为了简单起见,我把它放到了Activity组件里。
创建MediaPlayer有好几种方式,最简单的一种方式是调用MediaPlayer的静态方法create()方法,可以根据音频资源id或音频路径Uri来创建并准备好一个MediaPlayer对象。实现代码如下:
try {
//R.raw.baiyanglizan是存放到res/raw目录下的baiyanglizan.mp3音频文件资源id
mMediaPlayer =MediaPlayer.create(this, R.raw.baiyanglizan);
} catch(IllegalStateException e) {
e.printStackTrace();
}
有了MediaPlayer就可以控制音频了。
(1)准备音频:
mMediaPlayer.prepare();
(2)播放音频:
mMediaPlayer.start();
(3)暂停音频:
mMediaPlayer.pause();
(4)停止音频:
mMediaPlayer.stop();
(5)释放音频:
mMediaPlayer.release();
(6)注册音频播放完成监听器:
mMediaPlayer.setOnCompletionListener(newOnCompletionListener() {
@Override
public voidonCompletion(MediaPlayer mp) {
}
});
(7)注册音频播放出错监听器:
mMediaPlayer.setOnErrorListener(newOnErrorListener() {
@Override
public booleanonError(MediaPlayer mp, int what, int extra) {
return false;
}
});
为了满足用户自己控件什么时候播放,什么时候暂停音频的播放,界面提供了一个按钮,且在播放的时候会旋转,暂停的时候暂停音频并且停止旋转。所以一方面我们要在用户点击的时候实现音频的播放与暂停的切换控件,另一方面我们要通过旋转动画控制按钮的旋转。
(1) 按钮的旋转动画:
mPlayIb =(ImageButton) findViewById(R.id.ib_play);//获取旋转按钮控件
RotateAnimation mRotateAnim =null;
//创建旋转动画,第一个参数为起始角度,第二个参数为旋转角度,之后的参数控件旋转中心为控件中心
mRotateAnim = newRotateAnimation(0, 360, Animation.RELATIVE_TO_SELF,0.5f, Animation.RELATIVE_TO_SELF,0.5f);
mRotateAnim.setFillAfter(true);//动画播放完保持播放后的状态
mRotateAnim.setDuration(4000);//每旋转一圈设置为4秒
mRotateAnim.setRepeatCount(100);//重要旋转动画100次
mRotateAnim.setInterpolator(newLinearInterpolator());//动画匀速播放控制器
mPlayIb.startAnimation(mRotateAnim);//播放旋转动画
(2)旋转动画播放和停止时机与实现:
时机:音频播放,则动画播放,音频暂停或停止,则动画停止。
mPlayIb.startAnimation(mRotateAnim);//播放旋转动画
mPlayIb.clearAnimation();//停止旋转动画
(3)音频播放和暂停时机与实现:
时机一:界面一展示就开始播放音频,可以在MediaPlayer创建并准备好的时候就开始播放。
mMediaPlayer =MediaPlayer.create(this, R.raw.baiyanglizan);
mMediaPlayer.start();
时机二:用户点击播放按钮时,如果音频状态为暂停,则播放,如果音频状态是播放,则暂停。
boolean isPlay =true;//控制播放与暂停切换的boolean变量。初始为ture,表示界面一打开播放音频。
mPlayIb.setOnClickListener(newOnClickListener() {
@Override
public voidonClick(View v) {
if (!isPlay) {
mMediaPlayer.start();//播放音频
mPlayIb.startAnimation(mRotateAnim);//播放动画时机
} else {
mMediaPlayer.pause();//暂停音频
mPlayIb.clearAnimation();//停止动画时机
}
isPlay = !isPlay;
}
});
时机三:当Activity界面暂停时暂停音频与动画
@Override
protected voidonPause() {
if (mMediaPlayer.isPlaying()){
mMediaPlayer.pause();
mPlayIb.clearAnimation();
}
super.onPause();
}
时机四:当Activity销毁时释放音频与动画
@Override
protected voidonDestroy() {
mPlayIb.clearAnimation();
mMediaPlayer.stop();
mMediaPlayer.release();
super.onDestroy();
}