安卓学习日志 Day07

概述

放寒假了,有点晕车,因此多休息了一天。

今天继续构建 Miwok 语言应用,之前在 安卓学习日志 — Day05 中已经测试过了音频播放的功能,那么接下来就为Miwok 应用的每个类别添加上 单词的发音。

还是老样子,先做一个页面的(如:NumbersActivity) ,确保功能正常后 ,才按照同样的方法来更改其他的页面。

目标

  • 为每个单词添加音频
  • 点击某一单词时,立即正在播放的音频,而去播放被点击的单词的音频。
  • 当用户离开 某一 Activity 的页面时,立即停止播放
  • 与系统或其他应用交互

实现步骤

添加音频资源

这之前为每个页面的单词添加图片的方法基本一样。

更改 Word 类,添加一个成员变量 audioResourceId 用于存储 音频资源的ID,并更改构造函数以接收音频资源ID。

**值得注意的是 ,**需要同时更改两个构造函数:

一个接收 图片资源ID,而另一个不接收(Phrases页面没有图片,而另外 3个 页面有图片)

    /**
     * Create a new Word object.
     *
     * @param defaultTranslation is the word in a language that the user is already familiar with
     *                           (such as English)
     * @param miwokTranslation   is the word in the Miwok language
     * @param audioResourceId    is the audio resource id with the word
     */
    public Word(String defaultTranslation, String miwokTranslation, int audioResourceId) {
    
    
        this.defaultTranslation = defaultTranslation;
        this.miwokTranslation = miwokTranslation;
        this.audioResourceId = audioResourceId;
    }

    /**
     * Create a new Word object.
     *
     * @param defaultTranslation is the word in a language that the user is already familiar with
     *                           (such as English)
     * @param miwokTranslation   is the word in the Miwok language
     * @param imageResourceId    is the drawable resource ID for the image associated with the word
     * @param audioResourceId    is the audio resource id with the word
     */
    public Word(String defaultTranslation, String miwokTranslation, int imageResourceId, int audioResourceId) {
    
    
        this.defaultTranslation = defaultTranslation;
        this.miwokTranslation = miwokTranslation;
        this.imageResourceId = imageResourceId;
        this.audioResourceId = audioResourceId;
    }

然后将 所有单词的音频文件放在 res/raw 目录下,并更改 每个 Activity 中的数据源,为每个 Word 对象 添加 音频资源ID:

在这里插入图片描述

播放完成自动清空

当一个音频播放完成时应该 释放其占用的资源,为系统及其他应用的运行提供保障。

MediaPlayer 类的 可以绑定一个 OnCompletionListener 监听器,该监听器会在音频播放完成时回调 onCompletion 方法,因此播放完成时释放资源的逻辑就该写在 OnCompletionListener 监听器的 onCompletion 方法当中。

NumbersActivity 中自定义一个辅助方法 releaseMediaPlayer,用于释放 MedaiPlayer 所占用的资源:

    /**
     * Clean up the media player by releasing its resources.
     */
    private void releaseMediaPlayer() {
    
    
        // If the media player is not null, then it may be currently playing a sound.
        if (mediaPlayer != null) {
    
    
            // Regardless of the current state of the media player, release its resources
            // because we no longer need it.
            mediaPlayer.release();

            // Set the media player back to null. For our code, we've decided that
            // setting the media player to null is an easy way to tell that the media player
            // is not configured to play an audio file at the moment.
            mediaPlayer = null;
        }
    }
}

为 NumbersActivity 注册一个全局的 OnCompletionListener 监听器对象,并在 onCompletion 方法中调用刚刚自定义的辅助方法:

    /**
     * This listener gets triggered when the {@link MediaPlayer} has completed
     * playing the audio file.
     */
    private MediaPlayer.OnCompletionListener completionListener = new MediaPlayer.OnCompletionListener() {
    
    
        @Override
        public void onCompletion(MediaPlayer mp) {
    
    
            releaseMediaPlayer();
            Toast.makeText(NumbersActivity.this, "released.", Toast.LENGTH_SHORT).show();
        }
    };

最后只需使单击每个单词时,播放对应的音频资源即可,这使用到了 AdapterView.OnItemClickListener 监听器,因为 ListView 继承自 AdapterView ,当 ListView 中的任一列表项被点击时,都会回调 OnItemClickListener 监听器的 onItemClick 方法,因此 点击 单词并播放音频的逻辑代码应写在该方法当中。

下面为 ListView 根视图绑定一个 OnItemClickListener

**提示: ** 仅在调用 mediaPlayer.start() 方法之后再设置该回调,否则将无法触发该回调。

        // Set a click listener to play the audio when the list item is clicked on
        listView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    
    
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    
    
                // Get the {@link Word} object at the given position the user clicked on
                Word clickedWord = adapter.getItem(position);
                Toast.makeText(NumbersActivity.this, String.format("item %d clicked.", position), Toast.LENGTH_SHORT).show();

                // Create and setup the {@link MediaPlayer} for the audio resource associated with the current word
                mediaPlayer = MediaPlayer.create(NumbersActivity.this, clickedWord.getAudioResourceId());

                // Start the audio file
                mediaPlayer.start(); // no need to call prepare(); create() does that for you

                // Setup a listener on the media player, so that we can stop and release the
                // media player once the sound has finished playing.
                mediaPlayer.setOnCompletionListener(completionListener);
            }
        });

这时候,在 Numbers 页面中单击任意单词将播放其音频,并在播放完成时自动释放到音频播放所使用到的资源。

播放另一音频

现在 出现了一个问题,如果在一个单词读完之前单击了另一个单词,这时前一个单词的音频会和后一个单词的音频出现重叠。

这是因为在点击第二个单词时,上一个单词的音频 没有被清空掉,所以仍然继续播放。

解决方法也很简单,在单词任一单词时,首先释放之前的资源,然后在播放音频,因此,更改 onItemClick 方法如下:

            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    
    
                // Release the media player if it currently exists because we are about to
                // play a different sound file
                releaseMediaPlayer();

                // Get the {@link Word} object at the given position the user clicked on
                Word clickedWord = adapter.getItem(position);
                Toast.makeText(NumbersActivity.this, String.format("item %d clicked.", position), Toast.LENGTH_SHORT).show();

                // Create and setup the {@link MediaPlayer} for the audio resource associated with the current word
                mediaPlayer = MediaPlayer.create(NumbersActivity.this, clickedWord.getAudioResourceId());

                // Start the audio file
                mediaPlayer.start(); // no need to call prepare(); create() does that for you

                // Setup a listener on the media player, so that we can stop and release the
                // media player once the sound has finished playing.
                mediaPlayer.setOnCompletionListener(completionListener);
            }

现在,在一个单词读完之前单击了另一个单词,这时首先清空前一个单词的资源(即停止播放)然后再播放第二个单词的音频,这两个动作发生在一瞬间。

切换页面时

我们只希望用户在 这个 Activity 页面时能够播放音频,一旦离开了这个 Activity 页面就立即停止正在播放 的音频,并清空资源。

这就需要涉及到 Activity 的生命周期了,从官方文档中得知 当一个应用 失去焦点进入停止状态时,系统会连续得调用 onPause()onStop() 方法。

那么 ,只需 在 这两个方法的任意一个当中释放 音频资源即可,比如在 onStop 方法中:

    @Override
    protected void onStop() {
    
    
        super.onStop();
        // When the activity is stopped, release the media player resources because we won't
        // be playing any more sounds.
        releaseMediaPlayer();
        Toast.makeText(getApplicationContext(), "Activity stopped.", Toast.LENGTH_SHORT).show();
    }

现在,点击一个单词播放音频,在音频播放结束之前 ,迅速切换到其他的应用当中,音频将立即结束并释放资源。

应用交互

要在移动设备上做个良好公民,我们应该思考下,我们的应用该如何与系统及其他也想播放音频的应用互动。

Android 使用 Audio focus 来管理设备上的音频播放操作,这意味着任何时候只有掌握 Audio Focus 的应用才能播放音频。有时候就意味着暂停或播放我们应用中的音频一边其他更重要的音频能播放。

比如,在我们正在听音乐时,有人打电话过来,这时音乐会自动停止播放,并在通话结束之后自动继续播放。

Audio Focus 可以通过 Audio Manager 请求获得,而 Audio Manager 是一项系统服务,系统服务可以为所有应用都提供常见的功能,例如 通知服务 或闹钟管理器服务。某些系统服务可以让我们访问设备上的硬件组件,例如 位置信息管理器服务。

但最终,系统服务只是一组 java 类,可以像对待任何其他 Java 类一样通过获得对象实例然后对其调用方法来进行互动。

Audio Focus 也有属于自己的状态:

Audio Focus State Description of this state in your own words Describe what we should do in the Miwok app when we enter this state
AUDIOFOCUA_GAIN Gain audio focus back again (after having lost it earlier) Resume playing the audio file
AUDIOFOCUS_LOSS Permanent loss of audio focus Stop the MediaPlpayer and release resource
AUDIOFOCUS_LOSS_TRANSIENT Temporary loss of audio focus Pause audio file
AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK Temporary loss of audio focus, can “duck” or lower volume if applicable Pause audio file (each part of the word pronunciation is important to be heard)

我们应该做个良好公民,当 Audio Focus 状态发生变化时应该调整音频播放的行为,直接上代码吧。

首先在 NumbersActivity 中定义一个成员变,顾名思义 这个AudioManager 对象用于管理 Audio Focus:

    /**
     * Handles audio focus when playing a sound files
     **/
    private AudioManager audioManager;

一个作用于 Activity 全局的 audioFocusChangeListener :

    /**
     * This listener gets triggered whenever the audio focus changes
     * (i.e., we gain or lose audio focus because of another app or device).
     */
    AudioManager.OnAudioFocusChangeListener audioFocusChangeListener =
            new AudioManager.OnAudioFocusChangeListener() {
    
    
                public void onAudioFocusChange(int focusChange) {
    
    
                    if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
    
    
                        // The AUDIOFOCUS_LOSS case means we've lost audio focus and
                        // Stop playback and clean up resources
                        releaseMediaPlayer();
                        Toast.makeText(NumbersActivity.this, "AUDIOFOCUS_LOSS", Toast.LENGTH_SHORT).show();
                    } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT ||
                            focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
    
    
                        // The AUDIOFOCUS_LOSS_TRANSIENT case means that we've lost audio focus for a
                        // short amount of time. The AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK case means that
                        // our app is allowed to continue playing sound but at a lower volume. We'll treat
                        // both cases the same way because our app is playing short sound files.

                        // Pause playback and reset player to the start of the file. That way, we can
                        // play the word from the beginning when we resume playback.
                        mediaPlayer.pause();
                        mediaPlayer.seekTo(0);
                        Toast.makeText(NumbersActivity.this, "AUDIOFOCUS_LOSS_TRANSIENT", Toast.LENGTH_SHORT).show();
                    } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
    
    
                        // The AUDIOFOCUS_GAIN case means we have regained focus and can resume playback.
                        mediaPlayer.start();
                        Toast.makeText(NumbersActivity.this, "AUDIOFOCUS_GAIN", Toast.LENGTH_SHORT).show();
                    }
                }
            };

在 页面 创建时首先 实例化 AudioManager 对象, setContentView(R.layout.word_list); 之后添加一行 :

// Create and setup the {@link AudioManager} to request audio focus
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);

每当点击单词播放音频时,应该先判断当前是否具有 Audio Focus 对象,更改 点击列表项时被回调的 onItemClick 方法如下:

public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    
    
    // Get the {@link Word} object at the given position the user clicked on
    Word clickedWord = adapter.getItem(position);
    Toast.makeText(NumbersActivity.this, String.format("item %d clicked.", position), Toast.LENGTH_SHORT).show();

    // Release the media player if it currently exists because we are about to
    // play a different sound file
    releaseMediaPlayer();

    // Request audio focus for playback
    int result = audioManager.requestAudioFocus(audioFocusChangeListener,
            // Use the music stream.
            AudioManager.STREAM_MUSIC,
            // Request permanent focus.
            AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);

    if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
    
    
        // We have audio focus now.
        // Create and setup the {@link MediaPlayer} for the audio resource associated with the current word
        mediaPlayer = MediaPlayer.create(NumbersActivity.this, clickedWord.getAudioResourceId());

        // Start the audio file
        mediaPlayer.start(); // no need to call prepare(); create() does that for you

        // Setup a listener on the media player, so that we can stop and release the
        // media player once the sound has finished playing.
        mediaPlayer.setOnCompletionListener(completionListener);
    }
    if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
    
    
        releaseMediaPlayer();
    }
}

同样,当不再需要播放音频时,也要将 Audio Focus 释放掉,更改辅助方法 releaseMediaPlayer 方法如下:

private void releaseMediaPlayer() {
    
    
    // If the media player is not null, then it may be currently playing a sound.
    if (mediaPlayer != null) {
    
    
        // Regardless of the current state of the media player, release its resources
        // because we no longer need it.
        mediaPlayer.release();

        // Set the media player back to null. For our code, we've decided that
        // setting the media player to null is an easy way to tell that the media player
        // is not configured to play an audio file at the moment.
        mediaPlayer = null;

        // Regardless of whether or not we were granted audio focus, abandon it. This also
        // unregisters the AudioFocusChangeListener so we don't get anymore callbacks.
        audioManager.abandonAudioFocus(audioFocusChangeListener);
    }
}

最后 对 另外几个 页面 进行同样的更改即可。

现在 这几个 类型当中的重复代码变得越来越多, 因此,也可以考虑再写一个 Activity 作为 这些 Activity 的父类,将重复的代码都写在 父类 的 Activity 当中,如 全局变量,辅助方法这些。

总结

这次 对 Miwok 应用的更改都是针对某一时刻而执行的操作,如 播放音频时有短信通知等情况,不方便给出具体的实现效果,改天有空再考虑录个视频。

了解 安卓应用 中对于 音频 的管理,不用的音频类型有有不同的状态,每一种状态又会产生不一样的效果。

参考

android play music on opening app - Stack Overflow

MediaPlayer overview | Android Developers

These pages cover topics relating to recording, storing, and playing back audio and video.

MediaPlayer | Android Developers

Developer 开发推广大使 Ian Lake “正确的 Media Playback” - Big Android BBQ 演讲 | Youtube

AdapterView.OnItemClickListener

Asynchronous vs synchronous execution, what does it really mean?

MediaPlayer.OnCompletionListener

How to use setOnCompletionListener method in android.media.MediaPlayer

Managing audio focus | Android Developers

AudioFocusRequest | Android Developers

AudioManager | Android Developers

Managing audio focus | Android Developers

猜你喜欢

转载自blog.csdn.net/weixin_45075891/article/details/112667485