基础开发

引言

本章节为基于嵌入式硬件平台(以Quectel Pi智能主控板系列为例)的Android应用开发者提供全流程开发指导,涵盖开发环境搭建、调试工具使用、硬件相关API调用以及应用部署启动的核心操作。旨在帮助开发者快速入门嵌入式Android开发,解决从环境配置到应用落地的基础问题,同时适配嵌入式硬件的特殊开发需求,为后续复杂功能开发奠定技术基础。

开发环境

开发工具准备

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 DetailsNDK (Side by side)CMake,选择对应版本后点击“Apply”完成下载安装。
    Android studio NDK配置指引)
  • 在项目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
    APP调试指引)
  • 若未连接模拟器,想要手动生成debug版本或者带签名的release版本APK,可以在Android Studio的顶部工具栏build- Generate Signed App Bundle或者apk路径下选择“Android App Bundle” 或者“APK”。点击下一步后,系统将提示选择签名文件。可以选择已有的.jks或者.keystore文件,也可以新建一个签名文件。新建时需为签名文件命名(后缀默认为.jks)并选择存放路径。
    APP调试指引)

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

  • 完成签名配置后,选择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地址>,如果出现当前连接的设备信息,则表示连接成功。
    模拟器配置指引)