一、概述

1、课程设计内容

本次课程设计选择录音机为设计题目和内容,课程设计类型为不含数据库类。确定题目后,经过需求分析,可以得到一个安卓端录音机应该具备如下两个基本功能:录音执行与音频回放。以这两个功能为核心,使用Android studio开发,形成录音机应用的基本框架;此外,应用还具备如下附加功能:查看列表录音文件详情、停止当前录音、删除录音文件、重命名已有录音文件、显示播放进度等。使用JAVA语言进行开发,未设计数据库,所创建的录音文件保存在本地目录。

2、设计思路

应用分为如下几个模块来设计:页面资源模块,界面布局模块,权限获取模块,文件处理模块,录音执行模块。其中,页面资源模块将构成页面的各种所需的资源整合起来,比如图标图形,对话框的样式,各种可能会显示的文字等,方便后期开发将activity与资源ID绑定,或者在XML文件中引用各种素材资源文件;界面布局模块主要编写各种XML布局,包括主界面布局,权限获取界面布局,文件详情对话框,删除对话框和重命名对话框的设计。权限获取模块将麦克风使用权限、文件管理权限的获取封装成JAVA工具类,以便在主activity启动时调用权限;当主活动启动,文件管理权限通过之后,获取录音文件列表,并显示在界面上,此外,还提供文件的重命名,文件删除,文件详情获取、存储的功能;录音执行模块为整个应用系统的关键模块,用三个主要功能按钮设置点击监听,通过监听器启动,调用安卓的MediaRecorder API完成功能,不单独设置新的活动。在以上设计中,音频文件存储在JavaBean中,文件播放使用单独的服务实现,录制在主activity中进行,播放与录制操作相互独立。文件获取使用了adapter以便显示在列表中,文件详情与重命名使用了继承自对话框的类,音频的播放活动与主活动通过显式Intent消息传递对象进行通信。对于文件重命名和文件删除使用单独的工具类进行实现,工具类中设置有接口方便在主类中调用相关功能。

二、移动应用分析

1、在更新应用主界面列表时,出现页面无法更新,不显示的问题。

解决:Listview中adapter重写的过程出现错误。重写Baseadapter时,需要重写以下四个方法:getCount,getItem(int position),getItemId(int position),getView方法。getCount决定了listview一共有多少个item,而getView返回了每个item项所显示的view。

在重写的adapter中,先创建AudioBean集合对象mList,再通过类的构造方法初始化mList,在主类中新建adapter对象,再使用binding.audioLv.setAdapter(adapter)应用适配器,然后重写适配器中的getView方法即可,确保了及时更新列表录音文件信息。修改前后的效果图如 和 所示。

android 录音移动 android录音机_android


android 录音移动 android录音机_android_02

2、时间显示错误。在应用调试过程中,出现音频文件实际时长与显示时长不一致的情况,显示时长比实际时长多出8小时。
解决:在写文件信息工具类时,获取到的文件时长以毫秒计算。但是使用SimpleDateFormat将毫秒转化为格式化后的时分秒时,出现了时区错误。在相关函数内添加simpleDateFormat. setTimeZone(TimeZone.getTimeZone(“GMT+00:00”));解决了因时区的不同而转换时间不同的问题。
3、录音开始播放后,应用直接退出,程序崩溃,提示空指针异常。
解决:没有写明当应用退出时对应的操作逻辑。在主活动中重写onDestroy方法,销毁MediaRecorder对象,将连接对象取消绑定。
4、当创建一条新录音后,下方的录音列表不会实时更新。
解决:每当有新数据添加后,将List类型的列表清空,即mData.clear(),替换掉原来产生错误的“使List为null”的mData=null代码块,只进行一次初始化,排除错误的同时也减轻了系统开销。

三、移动应用界面设计
1、主界面设计
主界面采用RelativeLayout布局形式。布局较为简洁,上部分为录音控制面板,下部分为录音文件列表。
(1)列表布局:
基于对现有的录音机产品的分析,所展示的单个文件信息包括文件名、创建时间、持续时间、播放/暂停控制按钮等。同样采用相对布局,首先在drawable包内绘制背景素材,设置好边距,边框颜色等,然后在相对布局中将其作为背景使用;通过设置marginTop、align等属性完成布局,得到的布局页面如图所示。

图 3 布局

(2)ListView列表:使用一个可垂直滚动的视图集合,其中每个视图都紧跟在列表中的前一个视图之下。列表内容由重写的adapter动态生成。

图 4 列表
(3)录音模块设计:采用相对布局,包括录音控制按钮、状态提示信息、已录制时间展示等内容,为程序的主要部分。如下图所示。

图 6 录音界面

2、长按界面设计
在选中某个录音文件后长按,可以弹出菜单页面。包括三项基本功能:文件详情、文件删除、文件重命名。如图所示。

图 7 长按界面

3、详情界面设计

图 8 详情界面
详情页面AudioInfoDialog类继承自Dialog类,并通过绑定到CardView来显示界面。CardView组件界面设置有名称、时间、文件大小、路径四个信息展示,点击关闭按钮或对话框外区域关闭dialog。如图所示。
4、名称修改页面设计
此页面单独绘制,使用LinearLayout布局,由Textview、AppCompatEditText、两个按钮组成。“确定”、“取消”按钮水平排列在下方。页面布局如图所示。

图 9 重命名
主界面如图所示。

图 10 主界面
四、移动应用功能实现
1、录音功能
通过三个按钮添加监听器实现。在xml布局文件中添加三个对应按钮停止、录音、删除。为按钮添加背景图片,添加onClickListener点击事件。设置三个标志位,分别记录可能的状态:已停止、正在录音、已暂停录音。用户点击按钮可以实现不同的功能,已停止状态时,停止与删除按钮失效,点击录音,录音开始,录音按钮改为“暂停”,切换到录音状态;正在录音状态时,点击停止保存录音,点击暂停、删除按钮可以实现对应的功能。相关代码概要如下:

/**
     * 录音按钮
     */
    View.OnClickListener onClickListener1 = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            //点击录音按钮,关闭当前播放,开始录音
            audioService.closeMusic();
            for (AudioBean ab :
                    mData) {
                ab.setPaused(false); //标志位,标志当前是否暂停
                ab.setPlaying(false); //标志位,标志当前是否在播放
            }
            if (!isRecording && !isPaused) {
                startRecording();
                startTimer();//定时器,开始录音后实时更新已录制时间
                binding.audioIb.setImageResource(R.drawable.ic_pause_white);
                binding.state.setText("正在录音");
                isRecording = true;
            } else if (isRecording && !isPaused) {
                pauseRecording();
                binding.audioIb.setImageResource(R.drawable.ic_play_arrow_white);
                binding.state.setText("已暂停");
                isRecording = false;
                isPaused = true;
            } else {
                continueRecording();
                binding.state.setText("正在录音");
                binding.audioIb.setImageResource(R.drawable.ic_pause_white);
                isRecording = true;
                isPaused = false;
            }
        }
    };
    /**
     * 停止按钮
     */
    View.OnClickListener onClickListener2 = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if ((isPaused ^ isRecording) && mediaRecorder != null) {
                stopRecord();
                //刷新界面
                try {
                    getData();//获取数据列表函数
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
                adapter.notifyDataSetChanged();//通知适配器有更改发生
            }
        }
    };

    /**
     * 删除按钮,点击后撤销掉刚才的录音
     */
    View.OnClickListener onClickListener3 = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            if ((isPaused ^ isRecording) && mediaRecorder != null) {
                stopRecord();//停止录制
                boolean b = StorageUtils.deleteFileInAppDir(fileName);
                System.out.println("删除刚才录制的文件:" + b);
                adapter.notifyDataSetChanged();
            }
        }
    };

2、获取录音文件并展示在页面中
自定义adapter对象AudioListAdapter,重写相关的方法,使之在ListView的每一项都能获取到音频文件AudioBean的相关信息,主要表现在重写adapter的getView方法。Adapter的作用是动态获取并为列表添加项目。首先,使用一个 ViewHolder 对象缓存列表项的子 View,如果该列表项的 View 没有被创建过,使用 LayoutInflater 从布局文件 audio_layout 创建该 View;然后,根据列表项所代表的数据,更新列表项的各个子 View,如文本框的文字内容等。也包括根据列表项代表的音频文件的播放状态,更新列表项中的播放图标。列表项中的播放图标设置有点击监听器,以实现当用户点击播放图标时的交互操作。该监听器会回调 onItemPlayClick() 方法,以实现实际的播放操作。相关代码如下:

public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder = null;
        if (convertView == null) {
            convertView = LayoutInflater.from(context).inflate(R.layout.audio_layout, parent, false);
            viewHolder = new ViewHolder(convertView);
            convertView.setTag(viewHolder);
        } else {
            viewHolder = (ViewHolder) convertView.getTag();
        }
        AudioBean audioBean = mData.get(position);
        viewHolder.ab.tvTime.setText(audioBean.getTime());
        viewHolder.ab.tvDuration.setText(audioBean.getDuration());
        viewHolder.ab.tvTitle.setText(audioBean.getTitle());
        if (audioBean.isPlaying()) {//currently is playing
            viewHolder.ab.llController.setVisibility(View.VISIBLE);//显示
            viewHolder.ab.pbPlay.setMax(100);
            viewHolder.ab.pbPlay.setProgress(audioBean.getProgress());
            viewHolder.ab.ivPlay.setImageResource(R.drawable.ic_pause_white);
        } else {
            viewHolder.ab.ivPlay.setImageResource(R.drawable.ic_play_arrow_white);//当暂停或停止时显示另一个图标
            if (!audioBean.isPaused()){
                viewHolder.ab.llController.setVisibility(View.GONE);//隐藏当前图标
            }
        }
        View finalConvertView = convertView;
        viewHolder.ab.ivPlay.setOnClickListener(new View.OnClickListener() {//点击播放图标实现控制
            @Override
            public void onClick(View v) {
                if (onItemPlayClickListener != null) {
                    try {
                        onItemPlayClickListener.onItemPlayClick(AudioListAdapter.this, finalConvertView, v, position);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });
        return convertView;
    }

3、音频播放相关功能实现
为了使录音回放在后台进行,使用了安卓四大组件之一的Service组件。使用ServiceConnection接口,在Android中用于绑定服务(Service)时,监听服务连接状态的变化。本应用在Activity中定义了AudioService类,实现ServiceConnection接口,然后绑定AudioService类,以便在其他地方使用该服务。
通过实现onServiceConnected方法,可以得到一个实例,并且通过该实例,设置playChangedListener监听器。该监听器的作用是通知adapter更改;此外,在AudioService类中定义了音频的具体操作,如音频状态检测、播放、音频进度显示的具体操作。AudioService的相关代码如下:

/**
     * 音频播放
     */
    public void play(int position) throws IOException {//position为点击的位置
        if (mediaPlayer == null) {
            mediaPlayer = new MediaPlayer();
            //设置监听事件
            mediaPlayer.setOnCompletionListener(this);
        }
        //播放时获取当前歌曲列表,判断是否有歌曲
        mList = Contains.getMyAudioList();//Contain为自定义工具类
        if (mList.size() <= 0) {
            return;
        }
        if (mediaPlayer.isPlaying()) {
            mediaPlayer.stop();//停止播放
        }
        mediaPlayer.reset();
        playPosition = position;
        //设置音频资源路径
        mediaPlayer.setDataSource(mList.get(position).getPath());
        mediaPlayer.prepare();
        mediaPlayer.start();//开始播放
        mList.get(position).setPlaying(true);
        notifyActivityRefreshUI();
        updateProgress();
    }

    /**
     * 暂停或继续播放
     */
    public void pauseOrContinueMusic() {
        int playPosition = this.playPosition;
        AudioBean audioBean = mList.get(playPosition);
        if (mediaPlayer.isPlaying()) {
            mediaPlayer.pause();
            audioBean.setPlaying(false);
            audioBean.setPaused(true);
        } else {
            mediaPlayer.start();
            audioBean.setPlaying(true);
            audioBean.setPaused(false);
        }
        notifyActivityRefreshUI();
    }
    /**
     * 关闭音乐
     */
    public void closeMusic() {
        if (mediaPlayer != null) {
            mediaPlayer.stop();
            playPosition = -1;
        }
}

五、移动应用发布与运行展示
应用使用Android studio构建,使用Java 1.8开发,最小sdk版本为API24(Android 7.0),在Android10.0模拟器和真机上测试通过,修复错误,确保兼容性和性能。
应用发布:
1、点击导航栏“Build”-“Generate signed APK…”,在弹出的对话框中选择“APK”;
2、新建密钥,输入密码,开发者姓名等信息,保存。
3、生成apk,安装到模拟器或真机上运行。
运行时,可以手动在安卓设备的storage/emulated/0/MyRecord/recordings目录下添加音频,打开应用后,音频文件将实时更新到主页面列表中。支持的音频格式为MP3和m4a,可以手动更改。
六、总结
本次课程设计选择了“录音机”这一题目,类型为不包括数据库。本次题目涉及到的内容比较丰富,包括安卓四大组件、权限获取、文件存储、安卓录音api调用等。这些知识不是孤立存在而是相互联系的,不论是资源文件、布局文件,还是Activity类、接口实现、adapter实现、监听器实现,这些技巧贯穿于我的课程设计的始终。在设计中,一个清晰明了的设计思路和逻辑对我来说至关重要。代码本身是有规律的,一些代码规范可以通过查阅官方文档来解决,但是开发逻辑思路是需要重点去构思、策划,是应用设计中的关键。只有当我们确定了思路才可以又快又好地完成功能,而一个好的开发思路恰恰是我比较欠缺的。这也导致我在设计过程中出现了许多问题。比如前期设计音频播放时未设置一个音频文件的播放状态参数,导致切换其他音频后出现异常。我认为如果希望在移动应用领域有更好的发展,要在多接触项目的同时勤加练习,提高自己的开发水平。
对于本次作品,同样存在一些亮点,也有许多问题。本应用布局简洁,功能较为完整,设有进度条,可以在后台播放,对于录音进度有时间提示等。但是有一些问题尚未解决:当在最新Android12上测试时出现未知原因的空指针异常,缺失通知栏提醒、缺失批量删除、界面不够美观等。对我而言,这次课设令我体会到了严谨的意义与细节的重要性。一个小的代码上的疏忽会造成或多或少的令人疑惑的错误和异常。在以后的学习生活中,我会注意细节,在丰富知识水平的基础上提升自己。
七、参考文献
[1]. zhaihaohao1. Android中Service(onBind)和Activity之间通信[EB/OL]. [2019-08-02]. .
[2]. 菜鸟教程. 单例模式[EB/OL]. https://www.runoob.com/design-pattern/singleton-pattern.html.
[3]. 兰红, 李淑芝, 朱合隆. Android Studio移动应用开发从入门到实战[M]. 第2版. 清华大学出版社, 2022.