48、Android 画中画与音频录制播放开发指南

Android 画中画与音频录制播放开发指南

1. Android 画中画功能实现

1.1 进入画中画模式

要在 Android 应用中实现画中画(PiP)模式,首先需要在 MainActivity.java 类文件中添加 enterPipMode 的点击回调方法。以下是具体步骤:
1. 定位 MainActivity.java 文件,并在代码编辑器中打开。
2. 添加以下代码:

import android.app.PictureInPictureParams;
import android.util.Rational;

public void enterPipMode(View view) {
    Rational rational = new Rational(binding.videoView1.getWidth(),
            binding.videoView1.getHeight());
    PictureInPictureParams params =
            new PictureInPictureParams.Builder()
                    .setAspectRatio(rational)
                    .build();
    binding.pipButton.setVisibility(View.INVISIBLE);
    binding.videoView1.setMediaController(null);
    enterPictureInPictureMode(params);
}

此方法首先获取 Button 视图的引用,然后创建一个包含 VideoView 宽度和高度的 Rational 对象。接着使用 PictureInPictureParams.Builder 创建一组画中画参数,并将 Rational 对象作为视频播放的宽高比传入。由于在画中画模式下按钮不需要可见,所以将其设置为不可见,同时隐藏视频播放控件,确保在画中画模式下视频视图不受阻碍。

编译并在运行 Android 8 或更高版本的设备或模拟器上运行应用,等待视频播放开始后,点击画中画模式按钮,视频播放将缩小并显示在画中画窗口中。

1.2 检测画中画模式变化

为了检测画中画模式的变化,需要重写 onPictureInPictureModeChanged() 方法。在 MainActivity.java 文件中添加以下代码:

@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
    super.onPictureInPictureModeChanged(isInPictureInPictureMode);
    if (isInPictureInPictureMode) {
    } else {
        binding.pipButton.setVisibility(View.VISIBLE);
        binding.videoView1.setMediaController(mediaController);
    }
}

当该方法被调用时,会传入一个布尔值,指示活动是否处于画中画模式。代码通过检查此值来决定是否显示画中画按钮并重新激活播放控件。

1.3 添加广播接收器

为了在画中画窗口中添加一个操作,以显示包含当前播放视频名称的 Toast 消息,需要实现一个广播接收器,并使用待处理意图从画中画窗口向活动广播消息。在 onPictureInPictureModeChanged() 方法中添加以下代码:

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
    private BroadcastReceiver receiver;

    @Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode);
        if (isInPictureInPictureMode) {
            IntentFilter filter = new IntentFilter();
            filter.addAction(
              "com.ebookfrenzy.videoplayer.VIDEO_INFO");
            receiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, 
                                          Intent intent) {
                    Toast.makeText(context, 
                     "Favorite Home Movie Clips", 
                            Toast.LENGTH_LONG).show();
                }
            };
            registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);
        } else {
            binding.pipButton.setVisibility(View.VISIBLE);
            binding.videoView1.setMediaController(mediaController);
            if (receiver != null) {
                unregisterReceiver(receiver);
            }
        }
    }
}

1.4 添加画中画操作

1.4.1 准备图标文件

使用名为 ic_info_24dp.xml 的图像图标文件来表示画中画窗口中的操作。可以从以下 URL 下载源代码存档中的 project_icons 文件夹中找到该文件:
https://www.ebookfrenzy.com/retail/giraffejava/index.php
将该图标文件复制并粘贴到项目工具窗口中的 app -> res -> drawables 文件夹中。

1.4.2 创建意图和待处理意图

MainActivity.java 文件中添加以下方法来创建 Intent PendingIntent 对象:

import android.app.PendingIntent;
import static android.app.PendingIntent.FLAG_IMMUTABLE;

public class MainActivity extends AppCompatActivity {
    private static final int REQUEST_CODE = 101;

    private void createPipAction() {
        Intent actionIntent = 
              new Intent("com.ebookfrenzy.videoplayer.VIDEO_INFO");
        final PendingIntent pendingIntent = 
              PendingIntent.getBroadcast(MainActivity.this,
                REQUEST_CODE, actionIntent, FLAG_IMMUTABLE);
    }
}
1.4.3 创建远程操作对象

createPipAction() 方法中添加以下代码来创建 RemoteAction 对象:

import android.app.RemoteAction;
import android.graphics.drawable.Icon;
import java.util.ArrayList;

private void createPipAction() {
    final ArrayList<RemoteAction> actions = new ArrayList<>();
    Intent actionIntent = 
              new Intent("com.ebookfrenzy.videoplayer.VIDEO_INFO");
    final PendingIntent pendingIntent = PendingIntent.getBroadcast(
         MainActivity.this,
            REQUEST_CODE, actionIntent, FLAG_IMMUTABLE);
    final Icon icon = 
              Icon.createWithResource(MainActivity.this, 
                     R.drawable.ic_info_24dp);
    RemoteAction remoteAction = new RemoteAction(icon, "Info", 
              "Video Info", pendingIntent);
    actions.add(remoteAction);
    PictureInPictureParams params =
                new PictureInPictureParams.Builder()
                        .setActions(actions)
                        .build();
    setPictureInPictureParams(params);
}

最后,在 onPictureInPictureModeChanged() 方法中调用 createPipAction() 方法,以便在活动进入画中画模式时执行该操作。

1.5 测试画中画操作

重新运行应用并将活动置于画中画模式,点击画中画窗口,新的操作按钮将出现。点击操作按钮,等待 Toast 消息显示视频名称。

2. Android 音频录制与播放

2.1 音频播放

大多数 Android 实现支持多种音频格式,如 AAC LC/LTP、HE - AACv1(AAC +)、HE - AACv2(增强 AAC +)、AMR - NB、AMR - WB、MP3、MIDI、Ogg Vorbis 和 PCM/WAVE 等。音频播放可以使用 MediaPlayer AudioTrack 类。 MediaPlayer 类提供了更简单的编程接口,能满足大多数音频播放需求。

MediaPlayer 类的一些关键方法如下表所示:
| 方法 | 描述 |
| ---- | ---- |
| create() | 创建类的新实例,传入要播放音频的 Uri |
| setDataSource() | 设置音频播放的源 |
| prepare() | 指示播放器准备开始播放 |
| start() | 开始播放 |
| pause() | 暂停播放,可通过调用 resume() 方法恢复 |
| stop() | 停止播放 |
| setVolume() | 设置左右声道的播放音量 |
| resume() | 恢复之前暂停的播放会话 |
| reset() | 重置媒体播放器实例的状态 |
| release() | 释放播放器占用的资源 |

以下是一个典型的音频播放示例:

MediaPlayer mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource("https://www.yourcompany.com/myaudio.mp3");
mediaPlayer.prepare();
mediaPlayer.start();

2.2 音频录制

使用 MediaRecorder 类可以进行音频录制,该类的一些关键方法如下:
| 方法 | 描述 |
| ---- | ---- |
| setAudioSource() | 指定要录制的音频源,通常为设备麦克风 |
| setVideoSource() | 指定要录制的视频源,如摄像头 |
| setOutputFormat() | 指定录制的音频或视频的存储格式 |
| setAudioEncoder() | 指定录制音频的编码器 |
| setOutputFile() | 配置录制的音频或视频文件的存储路径 |
| prepare() | 准备 MediaRecorder 实例开始录制 |
| start() | 开始录制过程 |
| stop() | 停止录制过程,停止后需要重新配置和准备才能再次启动 |
| reset() | 重置录制器,需要重新配置和准备才能再次启动 |
| release() | 释放录制器占用的资源 |

以下是一个典型的音频录制示例:

MediaRecorder mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mediaRecorder.setOutputFile(audioFilePath);
mediaRecorder.prepare();
mediaRecorder.start();
// 录制结束
mediaRecorder.stop();
mediaRecorder.reset();
mediaRecorder.release();

要进行音频录制,应用的清单文件必须包含 android.permission.RECORD_AUDIO 权限:

<uses-permission android:name="android.permission.RECORD_AUDIO" />

2.3 示例项目创建

2.3.1 创建项目

从欢迎屏幕中选择“新建项目”选项,在新建项目对话框中选择“Empty Views Activity”模板,然后点击“下一步”。在“名称”字段中输入 AudioApp ,并指定 com.ebookfrenzy.audioapp 作为包名。在点击“完成”按钮之前,将“最低 API 级别”设置为 API 31:Android 12.0,将“语言”菜单设置为 Java,并按照第 11.8 节“迁移项目到视图绑定”中的步骤为项目添加视图绑定支持。

2.3.2 设计用户界面
  1. 项目创建完成后,从项目工具窗口中选择 activity_main.xml 文件,在布局编辑器的设计模式下,选择“Hello World!” TextView 并从布局中删除。
  2. 拖放三个 Button 视图到布局中,将按钮配置为显示“Play”、“Record”和“Stop”字符串资源,并分别为它们设置视图 ID 为 playButton recordButton stopButton
  3. 选择“Play”按钮,在“属性”面板中,将 onClick 属性配置为在用户选择时调用名为 playAudio 的方法。对其余按钮重复此步骤,分别配置为调用 recordAudio stopAudio 方法。

2.4 检查麦克风可用性

为了避免在没有麦克风的设备上录制音频时抛出异常,需要检查设备上是否存在麦克风。在 MainActivity.java 文件中添加以下方法:

package com.ebookfrenzy.audioapp;
import android.content.pm.PackageManager;

public class MainActivity extends AppCompatActivity {
    protected boolean hasMicrophone() {
        PackageManager pmanager = this.getPackageManager();
        return pmanager.hasSystemFeature(
                PackageManager.FEATURE_MICROPHONE);
    }
}

2.5 初始化活动

MainActivity.java 文件中修改 onCreate() 方法和添加 audioSetup() 方法:

package com.ebookfrenzy.audioapp;
import java.io.File; 
import java.io.IOException;
import androidx.annotation.NonNull; 
import android.media.MediaRecorder;
import android.os.Environment;
import android.media.MediaPlayer; 

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding; 
    private static MediaRecorder mediaRecorder;
    private static MediaPlayer mediaPlayer;
    private static String audioFilePath;
    private boolean isRecording = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        View view = binding.getRoot();
        setContentView(view);
        audioSetup();
    }

    private void audioSetup() {
        if (!hasMicrophone()) {
            binding.stopButton.setEnabled(false);
            binding.playButton.setEnabled(false);
            binding.recordButton.setEnabled(false);
        } else {
            binding.playButton.setEnabled(false);
            binding.stopButton.setEnabled(false);
        }
        File audioFile = new File(this.getFilesDir(), "myaudio.3gp");
        audioFilePath = audioFile.getAbsolutePath();
    }
}

此代码调用 hasMicrophone() 方法来确定设备是否包含麦克风。如果没有,则禁用所有按钮;否则,仅禁用“Stop”和“Play”按钮。同时,在应用的内部存储中创建一个名为 myaudio.3gp 的新文件来存储音频录制。

2.6 实现录制、停止和播放方法

2.6.1 录制方法
public void recordAudio (View view) {
    isRecording = true;
    binding.stopButton.setEnabled(true);
    binding.playButton.setEnabled(false);
    binding.recordButton.setEnabled(false);
    try {
        mediaRecorder = new MediaRecorder(this);
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mediaRecorder.setOutputFormat(
              MediaRecorder.OutputFormat.THREE_GPP);
        mediaRecorder.setOutputFile(audioFilePath);
        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
        mediaRecorder.prepare();
    } catch (Exception e) {
        e.printStackTrace();
    }
    mediaRecorder.start();
}
2.6.2 停止方法
public void stopAudio (View view) {
    binding.stopButton.setEnabled(false);
    binding.playButton.setEnabled(true);
    if (isRecording) {
        binding.recordButton.setEnabled(false);
        mediaRecorder.stop();
        mediaRecorder.release();
        mediaRecorder = null;
        isRecording = false;
    } else {
        mediaPlayer.release();
        mediaPlayer = null;
        binding.recordButton.setEnabled(true);
    }
}
2.6.3 播放方法
public void playAudio (View view) throws IOException {
    binding.playButton.setEnabled(false);
    binding.recordButton.setEnabled(false);
    binding.stopButton.setEnabled(true);
    mediaPlayer = new MediaPlayer();
    mediaPlayer.setDataSource(audioFilePath);
    mediaPlayer.prepare();
    mediaPlayer.start();
}

2.7 配置和请求权限

2.7.1 在清单文件中添加权限

AndroidManifest.xml 文件中添加以下权限标签:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
2.7.2 运行时请求权限

MainActivity.java 文件中添加以下代码:

import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.widget.Toast;
import android.Manifest;

public class MainActivity extends AppCompatActivity {
    private static final int RECORD_REQUEST_CODE = 101;

    protected void requestPermission(String permissionType, int requestCode) {
        int permission = ContextCompat.checkSelfPermission(this,
                permissionType);
        if (permission != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                    new String[]{permissionType}, requestCode
            );
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
            @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

此代码用于验证指定的权限是否已经被授予,如果未授予,则请求权限。当权限请求处理完成后, onRequestPermissionsResult() 方法将被调用。

2.8 运行时权限请求流程图

下面是运行时权限请求的 mermaid 流程图:

graph TD;
    A[开始] --> B[检查权限是否已授予];
    B -- 已授予 --> C[不请求权限];
    B -- 未授予 --> D[请求权限];
    D --> E[等待权限请求结果];
    E --> F[处理权限请求结果];
    F --> G[结束];

2.9 完整代码示例及说明

以下是 MainActivity.java 的完整代码示例,整合了前面提到的所有功能:

package com.ebookfrenzy.audioapp;

import java.io.File;
import java.io.IOException;
import androidx.annotation.NonNull;
import android.content.pm.PackageManager;
import android.media.MediaRecorder;
import android.os.Environment;
import android.media.MediaPlayer;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.widget.Toast;
import android.Manifest;
import android.view.View;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import com.ebookfrenzy.audioapp.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    private static MediaRecorder mediaRecorder;
    private static MediaPlayer mediaPlayer;
    private static String audioFilePath;
    private boolean isRecording = false;
    private static final int RECORD_REQUEST_CODE = 101;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        View view = binding.getRoot();
        setContentView(view);
        audioSetup();
        requestPermission(Manifest.permission.RECORD_AUDIO, RECORD_REQUEST_CODE);
    }

    private void audioSetup() {
        if (!hasMicrophone()) {
            binding.stopButton.setEnabled(false);
            binding.playButton.setEnabled(false);
            binding.recordButton.setEnabled(false);
        } else {
            binding.playButton.setEnabled(false);
            binding.stopButton.setEnabled(false);
        }
        File audioFile = new File(this.getFilesDir(), "myaudio.3gp");
        audioFilePath = audioFile.getAbsolutePath();
    }

    protected boolean hasMicrophone() {
        PackageManager pmanager = this.getPackageManager();
        return pmanager.hasSystemFeature(
                PackageManager.FEATURE_MICROPHONE);
    }

    public void recordAudio(View view) {
        isRecording = true;
        binding.stopButton.setEnabled(true);
        binding.playButton.setEnabled(false);
        binding.recordButton.setEnabled(false);
        try {
            mediaRecorder = new MediaRecorder(this);
            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
            mediaRecorder.setOutputFormat(
                    MediaRecorder.OutputFormat.THREE_GPP);
            mediaRecorder.setOutputFile(audioFilePath);
            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
            mediaRecorder.prepare();
        } catch (Exception e) {
            e.printStackTrace();
        }
        mediaRecorder.start();
    }

    public void stopAudio(View view) {
        binding.stopButton.setEnabled(false);
        binding.playButton.setEnabled(true);
        if (isRecording) {
            binding.recordButton.setEnabled(false);
            mediaRecorder.stop();
            mediaRecorder.release();
            mediaRecorder = null;
            isRecording = false;
        } else {
            mediaPlayer.release();
            mediaPlayer = null;
            binding.recordButton.setEnabled(true);
        }
    }

    public void playAudio(View view) throws IOException {
        binding.playButton.setEnabled(false);
        binding.recordButton.setEnabled(false);
        binding.stopButton.setEnabled(true);
        mediaPlayer = new MediaPlayer();
        mediaPlayer.setDataSource(audioFilePath);
        mediaPlayer.prepare();
        mediaPlayer.start();
    }

    protected void requestPermission(String permissionType, int requestCode) {
        int permission = ContextCompat.checkSelfPermission(this,
                permissionType);
        if (permission != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                    new String[]{permissionType}, requestCode
            );
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

代码说明:
- onCreate() 方法:在活动创建时进行初始化操作,包括视图绑定、音频设置和权限请求。
- audioSetup() 方法:检查设备是否有麦克风,并根据情况启用或禁用按钮,同时设置音频文件的存储路径。
- hasMicrophone() 方法:检查设备是否有麦克风。
- recordAudio() 方法:处理录制音频的逻辑,包括配置 MediaRecorder 并开始录制。
- stopAudio() 方法:处理停止录制或播放的逻辑,根据当前状态释放相应的资源。
- playAudio() 方法:处理播放音频的逻辑,包括配置 MediaPlayer 并开始播放。
- requestPermission() 方法:检查并请求所需的权限。
- onRequestPermissionsResult() 方法:处理权限请求的结果。

3. 总结

3.1 画中画功能总结

通过以上步骤,我们成功实现了 Android 应用中的画中画功能。从进入画中画模式、检测模式变化,到添加广播接收器和画中画操作,每一步都有明确的代码实现和操作步骤。画中画功能为用户提供了更加灵活的视频观看体验,在视频播放类应用中具有很大的实用价值。

3.2 音频录制与播放总结

在音频录制与播放方面,我们使用 MediaPlayer MediaRecorder 类实现了基本的音频录制和播放功能。同时,我们还考虑了设备麦克风的可用性检查、权限请求以及文件存储等问题。整个过程涵盖了从项目创建、界面设计到代码实现的各个方面,为开发者提供了一个完整的示例。

3.3 注意事项

  • 在使用画中画功能时,要确保设备运行的是 Android 8 或更高版本。
  • 进行音频录制时,一定要请求 android.permission.RECORD_AUDIO 权限,特别是在 Android 6 及更高版本的设备上,需要在运行时请求权限。
  • 在操作 MediaPlayer MediaRecorder 时,要注意资源的释放,避免出现内存泄漏等问题。

通过本文的介绍,开发者可以根据自己的需求对这些功能进行进一步的扩展和优化,开发出更加丰富和实用的 Android 应用。

【四轴飞行器】非线性三自由度四轴飞行器模拟器研究(Matlab代码实现)内容概要:本文围绕非线性三自由度四轴飞行器的建模仿真展开,重点介绍了基于Matlab的飞行器动力学模型构建控制系统设计方法。通过对四轴飞行器非线性运动方程的推导,建立其在三维空间中的姿态位置动态模型,并采用数值仿真手段实现飞行器在复杂环境下的行为模拟。文中详细阐述了系统状态方程的构建、控制输入设计以及仿真参数设置,并结合具体代码实现展示了如何对飞行器进行稳定控制轨迹跟踪。此外,文章还提到了多种优化控制策略的应用背景,如模型预测控制、PID控制等,突出了Matlab工具在无人机系统仿真中的强大功能。; 适合人群:具备一定自动控制理论基础和Matlab编程能力的高校学生、科研人员及从事无人机系统开发的工程师;尤其适合从事飞行器建模、控制算法研究及相关领域研究的专业人士。; 使用场景及目标:①用于四轴飞行器非线性动力学建模的教学科研实践;②为无人机控制系统设计(如姿态控制、轨迹跟踪)提供仿真验证平台;③支持高级控制算法(如MPC、LQR、PID)的研究对比分析; 阅读建议:建议读者结合文中提到的Matlab代码仿真模型,动手实践飞行器建模控制流程,重点关注动力学方程的实现控制器参数调优,同时可拓展至多自由度或复杂环境下的飞行仿真研究。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值