跳到主要内容

Android系统


📸 摄像头使用方法

以下示例展示了如何使用 Camera2 API 调用 Android 摄像头并进行预览。


1. 添加权限

AndroidManifest.xml 中添加所需的权限:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

2. 检查相机权限

安装好 App 后可以进入系统设置,默认授权 App 的访问相机权限,避免使用过程中需要的提示。


3. 创建 Camera2 预览

使用 Kotlin 开发,创建 CameraActivity.kt

import android.Manifest
import android.content.pm.PackageManager
import android.graphics.SurfaceTexture
import android.hardware.camera2.*
import android.os.*
import android.util.Log
import android.view.Surface
import android.view.TextureView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat

class CameraActivity : AppCompatActivity() {

private lateinit var textureView: TextureView
private lateinit var cameraManager: CameraManager

private var cameraDevice: CameraDevice? = null
private var captureSession: CameraCaptureSession? = null
private var previewRequestBuilder: CaptureRequest.Builder? = null
private var currentCameraId: String? = null

private val mainHandler = Handler(Looper.getMainLooper())

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
textureView = TextureView(this)
setContentView(textureView)

cameraManager = getSystemService(CAMERA_SERVICE) as CameraManager
textureView.surfaceTextureListener = textureListener
}

// TextureView 状态监听
private val textureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
Log.i(TAG, "SurfaceTexture available: ${width}x$height")
openCamera()
registerCameraAvailabilityCallback()
}

override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
Log.i(TAG, "SurfaceTexture size changed: ${width}x$height")
}

override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
Log.i(TAG, "SurfaceTexture destroyed")
closeCamera()
return true // 返回 true 表示我们不再使用此 SurfaceTexture
}

override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
}

// 注册 Camera 可用性监听
private fun registerCameraAvailabilityCallback() {
cameraManager.registerAvailabilityCallback(object : CameraManager.AvailabilityCallback() {
override fun onCameraAvailable(cameraId: String) {
Log.i(TAG, "Camera available: $cameraId")
}

override fun onCameraUnavailable(cameraId: String) {
Log.w(TAG, "Camera unavailable: $cameraId")
}
}, mainHandler)
}

// 打开前置摄像头
private fun openCamera() {
try {
for (cameraId in cameraManager.cameraIdList) {
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
currentCameraId = cameraId
break
}
}

if (currentCameraId == null) {
Log.e(TAG, "No camera found")
return
}

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 1)
return
}

Log.i(TAG, "Opening camera: $currentCameraId")
cameraManager.openCamera(currentCameraId!!, stateCallback, mainHandler)

} catch (e: Exception) {
Log.e(TAG, "openCamera error: ${e.message}", e)
}
}

// Camera 状态回调
private val stateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
Log.i(TAG, "Camera opened: ${camera.id}")
cameraDevice = camera
startPreview()
}

override fun onDisconnected(camera: CameraDevice) {
Log.w(TAG, "Camera disconnected: ${camera.id}")
camera.close()
cameraDevice = null
}

override fun onError(camera: CameraDevice, error: Int) {
Log.e(TAG, "Camera error ($error): ${camera.id}")
camera.close()
cameraDevice = null
}
}

// 启动预览
private fun startPreview() {
val camera = cameraDevice ?: return
val surfaceTexture = textureView.surfaceTexture ?: return

try {
surfaceTexture.setDefaultBufferSize(1920, 1080)
val surface = Surface(surfaceTexture)

previewRequestBuilder =
camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
addTarget(surface)
set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)
}

camera.createCaptureSession(listOf(surface),
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
Log.i(TAG, "CaptureSession configured")
captureSession = session
try {
session.setRepeatingRequest(previewRequestBuilder!!.build(), null, mainHandler)
} catch (e: CameraAccessException) {
Log.e(TAG, "setRepeatingRequest error: ${e.message}", e)
}
}

override fun onConfigureFailed(session: CameraCaptureSession) {
Log.e(TAG, "CaptureSession configure failed")
}
},
mainHandler
)
} catch (e: Exception) {
Log.e(TAG, "startPreview error: ${e.message}", e)
}
}

// 关闭摄像头资源
private fun closeCamera() {
try {
captureSession?.close()
captureSession = null
cameraDevice?.close()
cameraDevice = null
} catch (e: Exception) {
Log.e(TAG, "closeCamera error: ${e.message}", e)
}
}

override fun onPause() {
super.onPause()
Log.i(TAG, "onPause: closing camera")
closeCamera()
}

override fun onResume() {
super.onResume()
Log.i(TAG, "onResume: try reopen camera")
if (textureView.isAvailable) {
openCamera()
}
}

override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "onDestroy: releasing camera")
closeCamera()
}

companion object {
private const val TAG = "CameraActivity"
}
}

🎙 麦克风使用方法

使用麦克风进行音频拾取通常可以通过 MediaRecorderAudioRecord 来实现。 MediaRecorder 比较适合进行音频录制,而 AudioRecord 提供了更低级的控制,适用于需要精细控制音频采集的场景。


方法 1:使用 MediaRecorder 进行简单的音频录制

MediaRecorder 是一个高层次的接口,适合进行音频录制操作,简单易用。

步骤 1: 添加权限

AndroidManifest.xml 文件中添加录音权限:

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

步骤 2: 初始化 MediaRecorder

在代码中创建并配置 MediaRecorder

public class AudioRecorderActivity extends AppCompatActivity {
private MediaRecorder mediaRecorder;
private String audioFilePath;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_recorder);
// 声明按钮变量
Button startButton = findViewById(R.id.startButton);
Button stopButton = findViewById(R.id.stopButton);
//设置音频保存路径
audioFilePath = getExternalFilesDir(null).getAbsolutePath() + "/audio_record.aac";
// 初始化 MediaRecorder
mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 使用麦克风作为音频源
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); // 设置音频文件格式
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); // 设置音频编码方式
mediaRecorder.setOutputFile(audioFilePath); // 设置输出文件路径
// 步骤2:设置开始录音按钮的点击事件
startButton.setOnClickListener(v -> {
// 调用开始录音的方法
Log.i("wtf","startRecording");
startRecording();
});

// 步骤3:设置停止录音按钮的点击事件
stopButton.setOnClickListener(v -> {
// 调用停止录音的方法
Log.i("wtf","stopRecording");
stopRecording();
});
}
// 开始录音
public void startRecording() {
try {
mediaRecorder.prepare(); // 准备录音
mediaRecorder.start(); // 开始录音
} catch (IOException e) {
System.out.print("启动有异常");
e.printStackTrace();
}
}

// 停止录音
public void stopRecording() {
try {
mediaRecorder.stop(); // 停止录音
mediaRecorder.release(); // 释放资源
} catch (RuntimeException e) {
e.printStackTrace();
}
}

@Override
protected void onDestroy() {
super.onDestroy();
if (mediaRecorder != null) {
mediaRecorder.release(); // 确保释放资源
}
}
}

步骤 3: 开始和停止录音

你可以通过按钮或其他控件来控制录音的开始和停止,例如:

Button startButton = findViewById(R.id.startButton);
Button stopButton = findViewById(R.id.stopButton);

startButton.setOnClickListener(v -> startRecording());
stopButton.setOnClickListener(v -> stopRecording());

方法 2:使用 AudioRecord 进行低延迟音频录制

如果你需要更精确的音频控制,AudioRecord 是一个更底层的接口,适用于实时音频处理。

步骤 1: 添加权限

同样需要在 AndroidManifest.xml 文件中添加录音权限:

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

步骤 2: 初始化 AudioRecord

public class AudioRecorderActivity extends AppCompatActivity {
private static final int SAMPLE_RATE_IN_HZ = 44100; // 采样率
private AudioRecord audioRecord;
private boolean isRecording = false;
private Thread recordingThread;
private String audioFilePath;
private FileOutputStream fileOutputStream; // 文件输出流

@Override
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_recorder);
// 声明按钮变量
Button startButton = findViewById(R.id.startButton);
Button stopButton = findViewById(R.id.stopButton);
// AudioRecord 初始化
int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE_IN_HZ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);

audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
SAMPLE_RATE_IN_HZ,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize);

// 音频录制线程
recordingThread = new Thread(new Runnable() {
@Override
public void run() {
recordAudio();
}
});

startButton.setOnClickListener(v -> {
// 调用开始录音的方法
Log.i("wtf","startRecording");
startRecording();
});

// 步骤3:设置停止录音按钮的点击事件
stopButton.setOnClickListener(v -> {
// 调用停止录音的方法
Log.i("wtf","stopRecording");
stopRecording();
});
}

// 开始录音
public void startRecording() {
audioRecord.startRecording();
isRecording = true;
recordingThread.start();
}

// 停止录音
public void stopRecording() {
isRecording = false;
audioRecord.stop();
audioRecord.release();
}

private void recordAudio() {
// 创建音频文件路径(在外部存储的私有目录)
String audioFileName = "audio_record_" + System.currentTimeMillis() + ".wav";
File audioFile = new File(getExternalFilesDir(null), audioFileName);
audioFilePath = audioFile.getAbsolutePath(); // 保存文件路径到成员变量

byte[] audioBuffer = new byte[1024];
int totalBytesRead = 0;

try (FileOutputStream fos = new FileOutputStream(audioFile)) {
// 先写入WAV文件头(占位符,稍后填充实际值)
writeWavHeader(fos, 0, 0);

while (isRecording) {
int read = audioRecord.read(audioBuffer, 0, audioBuffer.length);
if (read > 0) {
// 将音频数据写入文件
fos.write(audioBuffer, 0, read);
totalBytesRead += read;
}
}

// 录制完成后,更新WAV文件头中的实际数据大小
updateWavHeader(audioFile, totalBytesRead);

} catch (IOException e) {
Log.e("AudioRecord", "File write failed: " + e.getMessage());
}
}

WAV 文件头相关代码

// 写入WAV文件头(初始占位符版本)
private void writeWavHeader(FileOutputStream out, long totalAudioLen, long totalDataLen) throws IOException {
long sampleRate = SAMPLE_RATE_IN_HZ;
int channels = 1; // 单声道
int bitsPerSample = 16; // 16位

byte[] header = new byte[44];

// RIFF/WAVE header
header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F';
header[4] = (byte) (totalDataLen & 0xff);
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);

// WAVE
header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E';

// 'fmt ' chunk
header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' ';

// 4 bytes: size of 'fmt ' chunk
header[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0;

// format = 1 (PCM)
header[20] = 1; header[21] = 0;

// number of channels
header[22] = (byte) channels; header[23] = 0;

// sample rate
header[24] = (byte) (sampleRate & 0xff);
header[25] = (byte) ((sampleRate >> 8) & 0xff);
header[26] = (byte) ((sampleRate >> 16) & 0xff);
header[27] = (byte) ((sampleRate >> 24) & 0xff);

// byte rate
long byteRate = sampleRate * channels * bitsPerSample / 8;
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);

// block align
header[32] = (byte) (channels * bitsPerSample / 8);
header[33] = 0;

// bits per sample
header[34] = (byte) bitsPerSample;
header[35] = 0;

// data chunk
header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a';
header[40] = (byte) (totalAudioLen & 0xff);
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);

out.write(header, 0, 44);
}

// 更新WAV文件头中的实际数据大小
private void updateWavHeader(File file, int totalAudioLen) {
try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
long totalDataLen = totalAudioLen + 36; // 36 = 44 - 8
raf.seek(4);
raf.write((int) (totalDataLen & 0xff));
raf.write((int) ((totalDataLen >> 8) & 0xff));
raf.write((int) ((totalDataLen >> 16) & 0xff));
raf.write((int) ((totalDataLen >> 24) & 0xff));

// 更新音频数据大小
raf.seek(40);
raf.write((int) (totalAudioLen & 0xff));
raf.write((int) ((totalAudioLen >> 8) & 0xff));
raf.write((int) ((totalAudioLen >> 16) & 0xff));
raf.write((int) ((totalAudioLen >> 24) & 0xff));
} catch (IOException e) {
Log.e("AudioRecord", "Error updating WAV header: " + e.getMessage());
}
}

@Override
protected void onDestroy() {
super.onDestroy();
if (audioRecord != null) {
audioRecord.release(); // 确保释放资源
}
}

步骤 3: 开始和停止录音

同样,使用按钮等控件来控制开始和停止录音:

Button startButton = findViewById(R.id.startButton);
Button stopButton = findViewById(R.id.stopButton);

startButton.setOnClickListener(v -> startRecording());
stopButton.setOnClickListener(v -> stopRecording());

方法 3:录音数据保存到文件

如果你希望将音频数据保存到文件中,可以通过 AudioRecordwrite 方法将音频流保存到文件,或者直接使用 MediaRecorder 保存为 .3gp 格式的文件。


其他注意事项

  1. 权限问题:同相机一致,建议使用系统设置默认开启麦克风权限,避免动态权限申请。
  2. 设备兼容性:确保设备支持麦克风功能,检查 AudioManager 和 MediaRecorder 的特性。
  3. 线程管理:如果使用 AudioRecord,需要在后台线程中进行音频录制,否则可能会导致 ANR(应用无响应)。
  4. 简单录音场景:推荐使用 MediaRecorder。
  5. 需要实时控制场景:使用 AudioRecord。

🖥️ SPI屏幕控制方法

支持微笑、星星眼、无语表情,眼球和眨眼控制。也可以设置自定义图片。

Android gradle 中添加上面的aar依赖

//把 .aar 文件放到模块的 libs/ 目录下
app/
├── libs/
│ └── libSpiScreen.aar
├── build.gradle
└── ...

//在模块的 build.gradle里加入:
dependencies {
implementation(files("./libs/libSpiScreen.aar"))
}

控制眼睛接口

// 打开spi屏幕
EyeAction.openSpiEye()
//控制眼睛的方法
EyeAction.blinkEye() // 眨眼1次
EyeAction.blinkEye(240, 2) // 眨眼2次
EyeAction.updateEmotionSmile() // 微笑
EyeAction.closeEye() // 闭眼
EyeAction.updateEmotionWuYu() // 无语
EyeAction.updateEmotionStar() // 星星眼

// 控制眼球位置,x:-100-100,y:-100-100 中心点0 y下正 x右正
//两个眼球位置同时控制
EyeAction.updateBothScleraXY( e0.x, e0.y, e1.x, e1.y )
//控制眼睛位置,0左眼 1右眼
EyeAction.updateScleraXY(0, e0.x, e0.y)

//关闭spi屏幕
EyeAction.closeSpiEye()

自定义眼睛图片

png图片转RGB565数据

// 打开两个屏幕
val open0Rst = GC9A01Native.open(0) // 屏幕0
val open1Rst = GC9A01Native.open(1) // 屏幕1

//绘制图片
val bitmapBytes = loadPngAsRGB565(this, R.raw.eye)
GC9A01Native.drawBitmap(
0,
0, 0, // x, y 起始位置
240, 240, // 宽高
bitmapBytes // 图片数据
)

GC9A01Native.drawBitmap(
1,
0, 0, // x, y 起始位置
240, 240, // 宽高
bitmapBytes // 图片数据
)

// 关闭两个屏幕
GC9A01Native.close(0) // 屏幕0
GC9A01Native.close(1) // 屏幕1