编写一个会讲绘本的安卓电视应用APP
Posted asmcvc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了编写一个会讲绘本的安卓电视应用APP相关的知识,希望对你有一定的参考价值。
背景
家里有孩子的基本上都逃脱不掉要给孩子看绘本讲绘本,无奈为父时间较少、普通话不标准、讲的效果也不好、嗓子经常性干哑、以及懒等各种理由。但是又想让孩子多听多看一些,就想着利用工具给孩子自动播放。
手机和PAD自然可行,但是这两种东西交互性太强了,小孩子容易拿来玩乱七八糟的东西,不容易专注,离得太近容易伤到眼睛。后来想到电视应用,电视的交互性差一些,比较适合,于是决定自己动手写个简单的TV应用。
总体目标是以最简单快捷的方式实现这个想法。
设想
- 创建一个简单的电视应用,全屏在电视上播放。可以参考「Android TV H5 电视应用」
- 考虑绘本的特点,要能同时播放绘本图画和声音。
- 搜集下载一批有声有色的绘本资源素材供本地使用。
- 电视的存储空间有限,资源素材以U盘形式存储,APP访问。
- 为方便扩展,U盘目录有序组织,设置一个根索引文件,方便配置。
- 小孩子不宜观看电视太久,可以扩展一个只播放儿童音乐、故事纯音频的功能。
- 既然可以只播放纯音频,又可以继续扩展给带孩子的老人播放戏曲,或者纯音乐的功能,这些是附加,不是主要的,能扩展即可。
- 既然绘本是同时播放图片和音频的,砍掉任意一个功能,又可以演变:只播放音频的功能上面提了,如果只播放图片的,可以应用为:幻灯片播放家庭照片,查看电子书、漫画等。这些是附加,不是主要的,能扩展即可。
实现
总体思路是,下载一批绘本资源、纯音频资源,分目录组织存放在U盘里。U盘根目录下创建一个文件夹专门存放这些资源,并设置一个菜单索引文件。App启动时自动遍历存储设备,根据文件特征判断是否是插入的U盘,并解析菜单索引文件展示菜单列表,支持遥控器上下左右按键选择菜单,选中后根据资源类型播放不同的资源。播放时支持遥控器的上下左右按键操控。
获取U盘路径
第一次编写电视应用,设备不在身旁,使用的是原生TV模拟器,这个TV模拟器有很多问题,声音播放不了。所以要试着猜想届时真机运行时的状态,一开始假想了很多场景,所幸的是最终拿到家里电视上安装运行的时候比较顺利,音频可以流畅播放。
为了方便获取U盘路径,在U盘根目录下探测名为tvbooks的文件夹是否存在,如果存在则认为是找到了U盘。
public class Settings {
private static String USBPath = null;// null; "/data/local/tmp"; //测试模拟器时的路径,release版本时使用null
// 放在U盘根目录的目录名
private static String RootName = "tvbooks";
// 根目录下的菜单配置文件名
public static String MenuFileName = "menu.txt";
private static String RootPath = null;
// 每个分类目录下放置一个目录索引,内容为:每行是一本书的名称,utf-8格式编码,以支持中文
public static String BookIndexFileName = "index.txt";
// 获取U盘路径
public static String getUSBPath(Context context) {
if (USBPath == null) {
// usb paths
List<String> usbPaths = UsbUtil.getStorageList();
for (int i = 0; i < usbPaths.size(); i++) {
File file = new File(usbPaths.get(i), RootName);
if (file.exists()) {
USBPath = usbPaths.get(i);
break;
}
}
if (USBPath == null) {
String sdcardPaths[] = UsbUtil.getVolumePaths(context);
if (sdcardPaths != null) {
for (int i = 0; i < sdcardPaths.length; i++) {
File file = new File(sdcardPaths[i], RootName);
if (file.exists()) {
USBPath = sdcardPaths[i];
break;
}
}
}
}
if (USBPath == null) {
String externalDir = Environment.getExternalStorageDirectory().getAbsolutePath();
File file = new File(externalDir, RootName);
if (file.exists()) {
USBPath = externalDir;
}
}
}
return USBPath;
}
public static String getRootPath(Context context) {
if (RootPath == null) {
RootPath = getUSBPath(context) + "/" + RootName;
}
return RootPath;
}
}
菜单列表
为了方便展示播放菜单,在这个tvbooks目录下创建一个菜单文件:menu.txt,用做配置,大致如下:
{
"menu": [{
"name": "绘本",
"type": "audio_image"
},
{
"name": "漫画",
"type": "audio_image"
},
{
"name": "故事",
"type": "audio",
"submenu": [
{
"name": "3-7岁故事"
},
{
"name": "儿童故事mp3"
},
{
"name": "儿童故事1-200"
},
{
"name": "儿童故事201-400"
},
{
"name": "儿童故事401-600"
},
{
"name": "儿童故事601-800"
},
{
"name": "儿童故事801及以后"
}
]
},
{
"name": "音乐",
"type": "audio",
"submenu": [
{
"name": "纯音乐"
}
]
},
{
"name": "歌曲",
"type": "audio",
"submenu": [
{
"name": "80年代经典老歌曲500首"
},
{
"name": "流行"
},
{
"name": "抖音神曲"
}
]
},
{
"name": "有声小说",
"type": "audio",
"submenu": [{
"name": "baishe"
},
{
"name": "sanguo"
},
{
"name": "xiyou"
}
]
},
{
"name": "戏曲",
"type": "audio",
"submenu": [{
"name": "郭永章河南坠子"
},
{
"name": "豫剧选段"
},
{
"name": "豫剧红脸王李世民游阴山"
}
]
}
]
}
-
name就是展示在应用里的菜单名,同时也是资源的文件夹名。
-
submenu为子菜单,最多为二级菜单,可选。
-
type预设三种类型:
- audio:纯音乐模式,说明该目录下仅是音频文件,循环播放目录下的音频文件即可。
- image:纯图片模式,说明该目录下仅是图片文件,幻灯片的方式播放图片即可。这个后来代码没有实现,因为生活中暂不需要。
- audio_image:绘本模式,说明该目录下是图声并存的绘本资源,需要展示图片同时播放音频。
tvbooks目录下的文件组织形式是这样的:
歌曲
故事
绘本
漫画
戏曲
音乐
有声小说
menu.txt
资源准备
从网上搜索查找同时有图和声音的绘本资源,找到了一个比较好的资源网站:波比在线-绘本馆, 全部下载下来,一共下载了一千多本,每个绘本以单独的文件夹存放。
另外找了上千首儿歌、故事,分目录存放。文件组织形式比较简单,参考了在线绘本的形式,某一页就是对应一个jpg和一个mp3文件,因此一个绘本目录下的资源文件是这样的:
1.jpg
1.mp3
2.jpg
2.mp3
3.jpg
3.mp3
4.jpg
4.mp3
5.jpg
5.mp3
……
切换下一本上一本,其实就是变更目录;切换上一页下一页,其实就是把序号变更下。实现起来都比较简单。
编写代码
基类
播放纯音乐的和播放绘本的功能分开实现(分别为:PlayAudioActivity、PlayHuibenActivity),但有一些复用的功能,可以抽离出来作为基类(PlayBaseActivity),方便复用代码,基类代码如下:
public abstract class PlayBaseActivity extends AppCompatActivity {
public static final int MSG_FILES_FOUND_OK = 0;
public static final int MSG_PLAY_NEXT = 1;
public static final int MSG_UPDATE_PROGRESS = 2;
protected SharedPreferences mSP;
protected ProgressBar progressBar;
protected MediaPlayer mediaPlayer = null;
// 是否自动播放
protected boolean isAutoPlayMode = true;
protected int currentPlayResIndex = 0;
protected int currentPlayIndex = 0;
// 记录上一次切换的时间
protected long lastChangeTime = 0;
// 本页有无音频
protected boolean isAudioExistThisPage = true;
// 如果自动播放图片,默认多少秒切换
protected int delaySecondsPerPage = 10;
// 音频播放完成后延迟的秒数,然后再自动播放下一个
protected int delaySecondsPerAudio = 4;
//更新UI
protected Runnable updateUI = null;
//主线程创建handler,在子线程中通过handler的post(Runnable)方法更新UI信息。
protected Handler handerUpdateUI = new Handler();
protected Handler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); //应用运行时,保持屏幕高亮,不锁屏
mSP = getSharedPreferences("cache", Context.MODE_PRIVATE);
isAutoPlayMode = mSP.getBoolean("isAutoPlay", true);
}
abstract protected void onAudioPlayCompletion();
abstract protected void turnNextRes(boolean isManualClick);
abstract protected void turnNextPage(boolean isManualClick);
public void setDelaySecondsPerPage(int seconds) {
this.delaySecondsPerPage = seconds;
}
public void setDelaySecondsPerAudio(int seconds) {
this.delaySecondsPerAudio = seconds;
}
public int getDelaySecondsPerAudio() {
return this.delaySecondsPerAudio;
}
//释放资源
@Override
protected void onDestroy() {
this.release();
super.onDestroy();
}
@Override
protected void onPause() {
this.release();
super.onPause();
}
protected void release() {
handerUpdateUI.removeCallbacks(updateUI);
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
}
}
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
int keyCode = event.getKeyCode();
int action = event.getAction();
return handleKeyEvent(action, keyCode) || super.dispatchKeyEvent(event);
}
private boolean handleKeyEvent(int action, int keyCode) {
if (action != KeyEvent.ACTION_DOWN)
return false;
switch (keyCode) {
case KeyEvent.KEYCODE_BACK:
case KeyEvent.KEYCODE_HOME: {
this.release();
}
break;
case KeyEvent.KEYCODE_ENTER:
case KeyEvent.KEYCODE_DPAD_CENTER:
//确定键enter
pausePlay();
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
//向下键
onKeyDownDownKey();
break;
case KeyEvent.KEYCODE_DPAD_UP:
//向上键
onKeyDownUpKey();
break;
case KeyEvent.KEYCODE_DPAD_LEFT:
//向左键
onKeyDownLeftKey();
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
//向右键
onKeyDownRightKey();
break;
default:
break;
}
return false;
}
protected void pausePlay() {
if (mediaPlayer != null) {
if (mediaPlayer.isPlaying()) {
mediaPlayer.pause();
isAutoPlayMode = false;
} else {
mediaPlayer.start();
}
}
}
protected void onKeyDownUpKey(){
currentPlayResIndex -= 2;
turnNextRes(true);
}
protected void onKeyDownDownKey(){
turnNextRes(true);
}
protected void onKeyDownLeftKey(){
currentPlayIndex -= 2;
turnNextPage(true);
}
protected void onKeyDownRightKey(){
turnNextPage(true);
}
protected void playAudio(String audioFilePath) {
if (new File(audioFilePath).exists()==false) {
isAudioExistThisPage = false;
return;
}else{
isAudioExistThisPage = true;
}
try {
if (mediaPlayer == null) {
mediaPlayer = new MediaPlayer();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mediaPlayer.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build());
} else {
mediaPlayer.setAudiostreamType(AudioManager.STREAM_MUSIC);
}
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mp.start();
if (progressBar != null) {
progressBar.setMax(mp.getDuration());
}
}
});
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
onAudioPlayCompletion();
}
});
} else {
if (mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
try {
mediaPlayer.reset();
} catch (Exception e) {
Toast.makeText(PlayBaseActivity.this, e.toString(), Toast.LENGTH_SHORT).show();
}
}
try {
mediaPlayer.setDataSource(audioFilePath);
mediaPlayer.prepareAsync();
} catch (Exception e) {
Toast.makeText(PlayBaseActivity.this, e.toString(), Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
Toast.makeText(PlayBaseActivity.this, e.toString(), Toast.以上是关于编写一个会讲绘本的安卓电视应用APP的主要内容,如果未能解决你的问题,请参考以下文章