基础开发
引言
本章节为基于嵌入式硬件平台(以Quectel Pi智能主控板系列为例)的Android应用开发者提供全流程开发指导,涵盖开发环境搭建、调试工具使用、硬件相关API调用以及应用部署启动的核心操作。旨在帮助开发者快速入门嵌入式Android开发,解决从环境配置到应用落地的基础问题,同时适配嵌入式硬件的特殊开发需求,为后续复杂功能开发奠定技术基础。
开发环境
开发工具准备
Android Studio下载与安装
- 官方下载地址:Android Studio官网
- 安装流程
- 依据操作系统(Windows/macOS/Linux)下载对应安装包;
- Windows系统:双击
.exe安装包启动向导; - macOS系统:挂载
.dmg镜像,将 Android Studio 拖拽至应用程序文件夹; - Linux系统:解压
.tar.gz包至/opt目录,执行/opt/android-studio/bin/studio.sh启动安装。
- 勾选“Android Studio”和“Android SDK”核心组件,选择非系统盘(建议预留≥20 GB 空间)作为安装路径,完成基础安装;
- 首次启动选择“Standard”标准配置,等待SDK及工具链自动下载完成。
嵌入式硬件驱动配置
针对Quectel Pi系列硬件,需额外完成驱动配置:
- Windows系统
请下载Quectel官方硬件驱动包,双击安装程序并按向导完成配置。
- Linux或macOS系统
系统通常已内置通用USB驱动,硬件连接后可被自动识别,一般无需手动安装额外驱动。
- 验证驱动:连接硬件至电脑,在Windows设备管理器或Linux终端执行
lsusb命令,若能识别到相应设备,则表示驱动配置成功。
NDK配置(用于底层硬件API调用)
- 打开Android Studio,进入
File > Settings > Appearance & Behavior > System Settings > Android SDK,切换至SDK Tools标签页。 - 勾选
Show Package Details、NDK (Side by side)和CMake,选择对应版本后点击“Apply”完成下载安装。

- 在项目
local.properties文件中配置NDK路径:ndk.dir=SDK安装路径/ndk/对应版本号
项目环境初始化
- 新建Android项目,选择“Empty Activity”模板,配置项目名称和包名,最低兼容Android版本建议≥Android 10(以适配嵌入式硬件)。
- 在
build.gradle(Module 级)文件中导入Quectel硬件SDK等相关依赖包(此步骤可选)。
ADB调试
工具配置
- ADB工具集成在Android SDK的
platform-tools目录中,需将该目录配置到系统环境变量:
- Windows:将
SDK路径\platform-tools添加至系统Path变量; - Linux/macOS:执行
echo 'export PATH=$PATH:SDK路径/platform-tools' >> ~/.bashrc,重启终端生效。
- 验证配置:在终端执行
adb version,若能正常显示版本信息,则表示配置成功。
嵌入式设备ADB连接
- 硬件设备端:进入系统设置,连续点击“版本号”7次,开启开发者选项,进入开发者选项后开启 “USB调试”和“USB安装”权限。
- 电脑端:通过USB线连接设备与电脑,在终端执行
adb devices,若列表中显示设备序列号,则表示连接成功;若未识别设备序列号,执行adb kill-server && adb start-server重启ADB服务。
常用ADB调试命令
| 功能场景 | 命令示例 | 说明 |
|---|---|---|
| 查看设备日志 | adb logcat |
实时输出设备运行日志,可通过grep过滤关键词(如 `adb logcat |
| 安装应用 | adb install本地APK路径 |
将本地编译的APK安装至设备,覆盖旧版本可加-r参数 |
| 进入设备 Shell | adb shell |
进入设备命令行,可执行底层硬件查询命令(如cat /sys/class/gpio/export) |
| 传输文件 | adb push本地路径 设备路径 |
向设备推送文件,反向传输使用adb pull设备路径 本地路径 |
| 重启设备 | adb reboot |
调试异常时重启嵌入式设备 |
基础API示例
GPIO控制API(嵌入式硬件核心功能)
GPIO简介
GPIO(General-Purpose Input/Output,通用输入/输出)是嵌入式系统和硬件开发中的一个基础且至关重要的概念。在Android设备上,它充当了系统与外部物理世界交互的“桥梁”,嵌入式Android的GPIO控制需结合硬件驱动,以下为基于系统文件操作的基础示例(以GPIO 12为例)。
权限配置
在AndroidManifest.xml中添加硬件访问权限:
<uses-permission android:name="android.permission.WRITE\_EXTERNAL\_STORAGE"/>
<uses-permission android:name="android.permission.READ\_EXTERNAL\_STORAGE"/>
java代码实现
通过java代码读写/sys/class/gpio下的文件通常需要root权限。因此,在常规的Android手机上进行这些操作非常困难,该方式通常仅适用于已获得root权限的设备或专为开发者设计的开发板。
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
public class GpioManager {
//GPIO操作基础路径
private static final String GPIO\_BASE\_PATH = "/sys/class/gpio/";
private final int gpioNum;
public GpioManager(int gpioNum) {
this.gpioNum = gpioNum;
exportGpio();
}
private void exportGpio() {
try {
File exportFile = new File(GPIO\_BASE\_PATH + "export");
BufferedWriter writer = new BufferedWriter(new FileWriter(exportFile));
writer.write(String.valueOf(gpioNum));
writer.close();
//默认设置为输出模式
setGpioDirection("out");
} catch (IOException e) {
e.printStackTrace();
}
}
/\*\*
\* 设置GPIO方向(in/out)
\* @param direction方向参数
\*/
public void setGpioDirection(String direction) {
try {
File dirFile = new File(GPIO\_BASE\_PATH + "gpio" + gpioNum + "/direction");
BufferedWriter writer = new BufferedWriter(new FileWriter(dirFile));
writer.write(direction);
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/\*\*
\* 控制GPIO电平(高电平1/低电平0)
\* @param value电平值
\*/
public void setGpioValue(int value) {
try {
File valueFile = new File(GPIO\_BASE\_PATH + "gpio" + gpioNum + "/value");
BufferedWriter writer = new BufferedWriter(new FileWriter(valueFile));
writer.write(String.valueOf(value));
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/\*\*
\* 释放GPIO引脚
\*/
public void unexportGpio() {
try {
File unexportFile = new File(GPIO\_BASE\_PATH + "unexport");
BufferedWriter writer = new BufferedWriter(new FileWriter(unexportFile));
writer.write(String.valueOf(gpioNum));
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
调用示例
//初始化GPIO 12引脚
GpioManager gpioManager = new GpioManager(12);
//设置GPIO为高电平
gpioManager.setGpioValue(1);
//业务逻辑执行完毕后释放GPIO
gpioManager.unexportGpio();
NDK代码实现
由于普通应用的Java/Kotlin代码运行在Android应用层,无法直接操作硬件,因此需通过JNI(Java Native Interface)进行桥接。调用C/C++编写的Native层代码实现GPIO控制(读写/sys/class/gpio文件或直接操作/dev/mem寄存器),并通过NDK将C/C++代码编译为.so动态链接库供Java层调用。
JNI层代码示例
#include <jni.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define GPIO_BASE_PATH "/sys/class/gpio/"
//导出GPIO引脚
static int gpio_export(int pin) {
char buffer[64];
int fd = open(GPIO_BASE_PATH "export", O_WRONLY);
if (fd < 0) {
perror("Failed to open export file");
return -1;
}
snprintf(buffer, sizeof(buffer), "%d", pin);
write(fd, buffer, strlen(buffer));
close(fd);
return 0;
}
//释放GPIO引脚
static int gpio_unexport(int pin) {
char buffer[64];
int fd = open(GPIO_BASE_PATH "unexport", O_WRONLY);
if (fd < 0) {
perror("Failed to open unexport file");
return -1;
}
snprintf(buffer, sizeof(buffer), "%d", pin);
write(fd, buffer, strlen(buffer));
close(fd);
return 0;
}
//设置GPIO方向(in/out)
static int gpio_set_direction(int pin, const char *dir) {
char buffer[64];
snprintf(buffer, sizeof(buffer), GPIO_BASE_PATH "gpio%d/direction", pin);
int fd = open(buffer, O_WRONLY);
if (fd < 0) {
perror("Failed to open direction file");
return -1;
}
write(fd, dir, strlen(dir));
close(fd);
return 0;
}
//设置GPIO电平(1/0)
static int gpio_set_value(int pin, int value) {
char buffer[64];
snprintf(buffer, sizeof(buffer), GPIO_BASE_PATH "gpio%d/value", pin);
int fd = open(buffer, O_WRONLY);
if (fd < 0) {
perror("Failed to open value file");
return -1;
}
char val_str[2] = {value + '0', '\0'};
write(fd, val_str, strlen(val_str));
close(fd);
return 0;
}
//JNI方法:初始化GPIO
JNIEXPORT jint JNICALL
Java_com_example_gpiocontrol_MainActivity_gpioInit(JNIEnv *env, jobject thiz, jint pin) {
if (gpio_export(pin) < 0) return -1;
if (gpio_set_direction(pin, "out") < 0) return -1;
return 0;
}
//JNI方法:设置GPIO电平
JNIEXPORT jint JNICALL
Java_com_example_gpiocontrol_MainActivity_gpioSetValue(JNIEnv *env, jobject thiz, jint pin, jint value) {
return gpio_set_value(pin, value);
}
//JNI方法:释放GPIO
JNIEXPORT jint JNICALL
Java_com_example_gpiocontrol_MainActivity_gpioRelease(JNIEnv *env, jobject thiz, jint pin) {
return gpio_unexport(pin);
}
CMakeLists配置
cmake_minimum_required(VERSION 3.22.1)
project("gpiocontrol")
# 添加共享库
add_library(
gpiocontrol
SHARED
gpio_control.c)
# 链接系统库
target_link_libraries(
gpiocontrol
log)
java层代码调用
package com.example.gpiocontrol;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
//加载Native库
static {
System.loadLibrary("gpiocontrol");
}
//声明native方法
private native int gpioInit(int pin);
private native int gpioSetValue(int pin, int value);
private native int gpioRelease(int pin);
private static final int GPIO_PIN = 12; //控制的GPIO引脚
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化GPIO
gpioInit(GPIO_PIN);
//高电平按钮
Button btnHigh = findViewById(R.id.btn_high);
btnHigh.setOnClickListener(v -> gpioSetValue(GPIO_PIN, 1));
//低电平按钮
Button btnLow = findViewById(R.id.btn_low);
btnLow.setOnClickListener(v -> gpioSetValue(GPIO_PIN, 0));
}
@Override
protected void onDestroy() {
super.onDestroy();
//释放GPIO
gpioRelease(GPIO_PIN);
}
}
Android App直接调用Shell(Runtime.exec)
在Java/Kotlin代码中通过Runtime.getRuntime().exec()执行Shell命令,以su权限读写/sys/class/gpio目录下的GPIO控制文件,实现电平控制。此方式需开发板具备Root权限。
核心代码示例
//初始化GPIO(导出引脚+设置输出方向)
try {
execShellCommand("su -c echo " + GPIO_PIN + " > /sys/class/gpio/export");
execShellCommand("su -c echo out > /sys/class/gpio/gpio" + GPIO_PIN + "/direction");
} catch (IOException e) {
e.printStackTrace();
}
//高电平按钮
Button btnHigh = findViewById(R.id.btn_high);
btnHigh.setOnClickListener(v -> {
try {
execShellCommand("su -c echo 1 > /sys/class/gpio/gpio" + GPIO_PIN + "/value");
} catch (IOException e) {
e.printStackTrace();
}
});
//低电平按钮
Button btnLow = findViewById(R.id.btn_low);
btnLow.setOnClickListener(v -> {
try {
execShellCommand("su -c echo 0 > /sys/class/gpio/gpio" + GPIO_PIN + "/value");
} catch (IOException e) {
e.printStackTrace();
}
});
/**
* 执行Shell命令
*/
private void execShellCommand(String command) throws IOException {
Runtime.getRuntime().exec(command);
}
厂商提供的专用API或Jar包
多数厂商会基于底层驱动封装标准化的Java API(以Jar包或SDK形式提供)。开发者可从厂商处获取相应的SDK,将其集成到Android应用中,即可直接调用封装好的API来控制GPIO,无需关注底层硬件操作的具体实现。
摄像头API(基于Camera2原生API)
以下为基于Android原生Camera2 API的实现示例,适配嵌入式设备单摄像头场景,支持预览和拍照功能。
依赖配置
Camera2为Android原生API,无需导入第三方依赖。仅需在build.gradle(Module 级)中确认最低兼容版本不低于Android 5.0(API 21)。
权限与布局配置
- 权限声明(AndroidManifest.xml):
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.WRITE\_EXTERNAL\_STORAGE"/>
<uses-permission android:name="android.permission.READ\_EXTERNAL\_STORAGE"/>
<-- 摄像头硬件特性 -->
<uses-feature android:name="android.hardware.camera.any" android:required="true"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
- 布局文件(activity_camera2.xml):
采用TextureView作为预览载体,适配嵌入式设备屏幕尺寸:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 摄像头预览视图 -->
<TextureView
android:id="@+id/textureView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 拍照按钮 -->
<Button
android:id="@+id/takePhotoBtn"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginBottom="30dp"
android:text="拍照"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
代码实现
public class Camera2Activity extends AppCompatActivity {
//屏幕旋转角度映射
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
static {
ORIENTATIONS.append(Surface.ROTATION_0, 90);
ORIENTATIONS.append(Surface.ROTATION_90, 0);
ORIENTATIONS.append(Surface.ROTATION_180, 270);
ORIENTATIONS.append(Surface.ROTATION_270, 180);
}
//核心组件
private TextureView textureView;
private Button takePhotoBtn;
private CameraManager cameraManager;
private CameraDevice cameraDevice;
private CameraCaptureSession cameraCaptureSession;
private CaptureRequest.Builder previewRequestBuilder;
private Size previewSize;
private ImageReader imageReader;
private Handler backgroundHandler;
private HandlerThread backgroundThread;
//摄像头ID(默认后置)
private String cameraId;
//照片保存路径
private static final String PHOTO_SAVE_PATH = "/sdcard/DCIM/Camera2/";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera2);
initView();
initBackgroundThread();
initCameraManager();
createPhotoDir();
//监听TextureView状态
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
if (checkPermissions()) {
openCamera(width, height);
} else {
ActivityCompat.requestPermissions(Camera2Activity.this,
new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1001);
}
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
configureTransform(width, height);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {}
});
//拍照按钮点击事件
takePhotoBtn.setOnClickListener(v -> takePhoto());
}
/**
* 初始化视图控件
*/
private void initView() {
textureView = findViewById(R.id.textureView);
takePhotoBtn = findViewById(R.id.takePhotoBtn);
}
/**
* 初始化后台线程(用于处理摄像头异步操作)
*/
private void initBackgroundThread() {
backgroundThread = new HandlerThread("Camera2BackgroundThread");
backgroundThread.start();
backgroundHandler = new Handler(backgroundThread.getLooper());
}
/**
* 初始化摄像头管理器
*/
private void initCameraManager() {
cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
try {
//获取后置摄像头ID
for (String id : cameraManager.getCameraIdList()) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (facing != null && facing == CameraCharacteristics.LENS_FACING_BACK) {
cameraId = id;
//获取预览尺寸
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (map != null) {
previewSize = map.getOutputSizes(SurfaceTexture.class)[0];
//初始化ImageReader用于拍照(JPEG格式,最多缓存1张)
imageReader = ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(),
ImageFormat.JPEG, 1);
imageReader.setOnImageAvailableListener(reader -> {
//处理拍摄的照片
Image image = reader.acquireNextImage();
savePhoto(image);
image.close();
}, backgroundHandler);
}
break;
}
}
} catch (CameraAccessException e) {
e.printStackTrace();
Toast.makeText(this, "初始化摄像头失败", Toast.LENGTH_SHORT).show();
}
}
/**
* 创建照片保存目录
*/
private void createPhotoDir() {
File dir = new File(PHOTO_SAVE_PATH);
if (!dir.exists()) {
boolean isCreated = dir.mkdirs();
if (!isCreated) {
Toast.makeText(this, "照片目录创建失败", Toast.LENGTH_SHORT).show();
}
}
}
/**
* 检查权限(摄像头+存储)
*/
private boolean checkPermissions() {
return ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
&& ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
}
/**
* 打开摄像头
*/
private void openCamera(int width, int height) {
try {
//权限二次校验
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}
//打开摄像头(异步回调)
cameraManager.openCamera(cameraId, new CameraDevice.StateCallback() {
@Override
public void onOpened(@NonNull CameraDevice camera) {
cameraDevice = camera;
//摄像头打开成功,创建预览会话
createCameraPreviewSession();
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
camera.close();
cameraDevice = null;
}
@Override
public void onError(@NonNull CameraDevice camera, int error) {
camera.close();
cameraDevice = null;
Toast.makeText(Camera2Activity.this, "摄像头打开失败:" + error, Toast.LENGTH_SHORT).show();
}
}, backgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
/**
* 创建摄像头预览会话
*/
private void createCameraPreviewSession() {
try {
SurfaceTexture texture = textureView.getSurfaceTexture();
//设置预览缓冲区大小
texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
//创建预览Surface
Surface previewSurface = new Surface(texture);
//构建预览请求
previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
previewRequestBuilder.addTarget(previewSurface);
//创建拍照会话(包含预览和ImageReader的Surface)
List<Surface> surfaces = new ArrayList<>();
surfaces.add(previewSurface);
surfaces.add(imageReader.getSurface());
cameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
if (cameraDevice == null) {
return;
}
cameraCaptureSession = session;
try {
//设置自动对焦
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//启动预览(持续请求)
cameraCaptureSession.setRepeatingRequest(previewRequestBuilder.build(),
new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
}
}, backgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
Toast.makeText(Camera2Activity.this, "预览会话配置失败", Toast.LENGTH_SHORT).show();
}
}, backgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
/**
* 调整预览画面的旋转和比例(适配不同屏幕方向)
*/
private void configureTransform(int viewWidth, int viewHeight) {
if (previewSize == null || textureView == null) {
return;
}
int rotation = getWindowManager().getDefaultDisplay().getRotation();
Matrix matrix = new Matrix();
RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
RectF bufferRect = new RectF(0, 0, previewSize.getHeight(), previewSize.getWidth());
float centerX = viewRect.centerX();
float centerY = viewRect.centerY();
if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY());
matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
float scale = Math.max(
(float) viewHeight / previewSize.getHeight(),
(float) viewWidth / previewSize.getWidth());
matrix.postScale(scale, scale, centerX, centerY);
matrix.postRotate(90 * (rotation - 2), centerX, centerY);
} else if (Surface.ROTATION_180 == rotation) {
matrix.postRotate(180, centerX, centerY);
}
textureView.setTransform(matrix);
}
/**
* 执行拍照逻辑
*/
private void takePhoto() {
if (cameraDevice == null || cameraCaptureSession == null) {
Toast.makeText(this, "摄像头未就绪", Toast.LENGTH_SHORT).show();
return;
}
try {
//构建拍照请求
CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(imageReader.getSurface());
//设置拍照参数(自动对焦、自动曝光)
captureBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
//调整照片旋转角度
int rotation = getWindowManager().getDefaultDisplay().getRotation();
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATIONS.get(rotation));
//停止预览,执行拍照
cameraCaptureSession.stopRepeating();
cameraCaptureSession.capture(captureBuilder.build(), new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
//拍照完成后恢复预览
try {
session.setRepeatingRequest(previewRequestBuilder.build(), null, backgroundHandler);
Toast.makeText(Camera2Activity.this, "拍照成功", Toast.LENGTH_SHORT).show();
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
}, backgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
Toast.makeText(this, "拍照失败", Toast.LENGTH_SHORT).show();
}
}
/**
* 保存照片到本地
*/
private void savePhoto(Image image) {
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
//生成唯一文件名(时间戳)
String fileName = System.currentTimeMillis() + ".jpg";
File photoFile = new File(PHOTO_SAVE_PATH + fileName);
try (FileOutputStream fos = new FileOutputStream(photoFile)) {
fos.write(bytes);
runOnUiThread(() -> Toast.makeText(this, "照片已保存:" + photoFile.getPath(), Toast.LENGTH_SHORT).show());
} catch (IOException e) {
e.printStackTrace();
runOnUiThread(() -> Toast.makeText(this, "照片保存失败", Toast.LENGTH_SHORT).show());
}
}
/**
* 权限申请回调
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 1001) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
//权限申请成功,打开摄像头
if (textureView.isAvailable()) {
openCamera(textureView.getWidth(), textureView.getHeight());
}
} else {
Toast.makeText(this, "需要摄像头和存储权限才能使用", Toast.LENGTH_SHORT).show();
finish();
}
}
}
/**
* 释放资源
*/
private void closeCamera() {
if (cameraCaptureSession != null) {
cameraCaptureSession.close();
cameraCaptureSession = null;
}
if (cameraDevice != null) {
cameraDevice.close();
cameraDevice = null;
}
if (imageReader != null) {
imageReader.close();
imageReader = null;
}
}
/**
* 停止后台线程
*/
private void stopBackgroundThread() {
if (backgroundThread != null) {
backgroundThread.quitSafely();
try {
backgroundThread.join();
backgroundThread = null;
backgroundHandler = null;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
closeCamera();
stopBackgroundThread();
}
}
应用部署和启动
应用部署
- 点击Android Studio工具栏中的绿色运行按钮,IDE将自动执行预定义的构建脚本,完成APK Debug版本的编译、打包与安装。前提条件:当前需已连接模拟器或真机设备,否则会提示“无可用设备”。生成的APK文件位于
项目文件路径/app/build/intermediates/apk/debug/app-debug.apk

- 若未连接模拟器,想要手动生成debug版本或者带签名的release版本APK,可以在Android Studio的顶部工具栏build- Generate Signed App Bundle或者apk路径下选择“Android App Bundle” 或者“APK”。点击下一步后,系统将提示选择签名文件。可以选择已有的.jks或者.keystore文件,也可以新建一个签名文件。新建时需为签名文件命名(后缀默认为.jks)并选择存放路径。

然后设置您的签名密码与密钥别名(Alias,即密钥的自定义标识,可自行命名),完成后点击“OK”,系统将自动创建签名文件。

- 完成签名配置后,选择release或者debug包,系统将使用当前配置的签名文件进行打包。生成的已签名APK文件默认位于
项目文件路径/app/release目录下。
应用启动
模拟器启动
首次启动项目时,Android Studio通常会默认创建一个模拟器,直接点运行按钮即可启动项目。若需使用指定版本或分辨率的模拟器,需手动创建。
- 在Android Studio的工具栏中找到“Device Manager”,点击“Create Virtual Device”,选择需要的模拟器配置,点击“Next”。


真机启动
- 首先使用USB线连接电脑和手机。进入手机“设置”-“关于手机”,连续点击“版本号”7次,直到出现“您已处于开发者模式”的提示。返回设置菜单,进入“开发者选项”,开启“USB调试”(不同机型路径可能略有差异,但大致操作一样)。此时可能会弹出授权框,点击“允许”即可。

- 此时在终端输入命令adb devices,如果出现当前连接的设备信息,则表示连接成功。
无线连接可摆脱线缆束缚,但通常需先用USB完成初始设置。具体操作因手机Android版本而异。
- 请确保手机和电脑处于同一局域网内,并在手机上开启开发者模式、USB调试和无线调试;
- 查看当前手机的IP地址,在终端输入命令adb connect <ip地址>,如果出现当前连接的设备信息,则表示连接成功。
