简介:基于Java和Kotlin编写的DJI Mobile SDK Android V5开发版无人机Demo源码,面向Android开发者、无人机应用初学者及需要二次集成的工程师。包体共2000个文件、约44MB,其中776个XML负责界面布局与配置,460个Java与332个Kotlin源码承担主要业务逻辑和飞控调用,另有PNG/webp图片、MP3/WAV音频及Gradle、JSON等构建与数据资源;目录按uxsdk、as、sample等模块划分,便于对照学习。目前已有430人学习下载。通过研读源码,可以掌握Mobile SDK的接口调用方式,理解无人机操控、UI交互和自动飞行模块的组织思路;readme还提供了快速上手指引,开源结构也支持按需修改或集成到自有项目,是学习无人机Android开发的实用参考。
1. DJI Mobile SDK Android V5 开发版 Demo 源码包:2137 个文件里真正决定上手速度的是四类东西
第一次打开这份基于 Java 和 Kotlin 的 DJI Mobile SDK Android V5 开发版 Demo 源码包,绝大多数人会被文件数量震住:2137 个文件、776 个 XML、460 个 Java 源文件、396 个 Kotlin 源文件,外加几百张图片和音频,看着就是一个黑匣子。但把目录结构完整过一遍就会发现,真正决定你能不能编译通过、能不能在真机上跑起飞行链路、能不能改造成自己项目的,其实只有四类东西:Gradle 构建链、三个 SDK 模块(as / uxsdk / sample)、地图组件(MapWidget 与 HSIMarkerLayer)、以及 Java/Kotlin 混编时的资源组织规律。这篇笔记就按这条线拆,每一步都能照着复现。适合想把 V5 Demo 跑起来做二次开发的 Android 工程师,也适合想摸清 DJI Mobile SDK 模块边界的初上手开发者。
2. 工程结构拆解:Gradle 构建链与三个 SDK 模块的职责边界
2.1 从 settings.gradle 到 gradlew:构建入口与依赖来源
一个 2137 文件的 Android 工程,读代码之前必须先搞清它由几个模块组成、依赖从哪来。这个 Demo 的模块注册表写在根目录的 settings.gradle 里,结构如下:
// settings.gradle —— 模块注册与仓库配置 pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } } dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() } } rootProject.name = "DJI Mobile SDK V5 Demo" include ':android-sdk-v5-uxsdk' include ':android-sdk-v5-as' include ':android-sdk-v5-sample'逻辑说明:settings.gradle 在构建流程里第一个被解析,它做两件事——声明依赖仓库和注册子模块。dependencyResolutionManagement里的FAIL_ON_PROJECT_REPOS值得单独拎出来说,它强制所有子模块的依赖都必须走根工程声明的仓库,子模块自己写的repositories会被拒绝。这是新版 Gradle 收敛依赖来源的常见做法。如果你把这段配置搬到自己的工程,又在一个子模块的 build.gradle 里写了repositories { maven { url ... } },构建会直接失败,报错文本里会出现类似Repository ... is not allowed的提示。
模块注册表里 include 了三行,映射到根工程下三个同名目录:android-sdk-v5-uxsdk、android-sdk-v5-as、android-sdk-v5-sample。这三个目录分别对应 UI 组件库、自动飞行能力模块和示例应用,各自职责在 2.2 节展开。
接着是构建入口。工程带了 gradlew、gradlew.bat 和 gradle-wrapper.jar,wrapper 的作用是锁定 Gradle 发行版版本,让所有人在不同机器上构建出同一个结果。常用构建命令如下:
# Linux / macOS 下编译示例 app ./gradlew :android-sdk-v5-sample:assembleDebug # Windows 下同一条命令 gradlew.bat :android-sdk-v5-sample:assembleDebug # 查看某个模块的完整依赖树 ./gradlew :android-sdk-v5-sample:dependencies --configuration debugRuntimeClasspath参数说明::android-sdk-v5-sample:assembleDebug由模块路径和任务名组成,靠冒号分隔。assembleDebug 会编译该模块全部依赖并打出 debug 包,APK 产物在 android-sdk-v5-sample/build/outputs/apk/debug/ 目录下。首次构建建议先执行./gradlew help验证 wrapper 能不能正常解析 Gradle 发行版,再跑 dependencies 命令看依赖树。依赖树里每一行带箭头标记的条目都代表版本冲突,排查 Mapbox 或 AppCompat 版本异常时,这份输出比反复读 build.gradle 直观得多。
初次构建最容易卡在 wrapper 下载发行版这一步。gradle-wrapper.properties 里写了 Gradle 发行版的下载地址,网络不稳时终端会长时间停在Downloading gradle-...zip,最后报连接超时。这不是代码问题,是环境问题,解决办法在第 5 章避坑部分给出。总之记住一个原则:先让构建链变绿,再谈业务代码,否则后面每次编译报错都会混入环境噪音,很难定位。
2.2 android-sdk-v5-as / uxsdk / sample:三兄弟各自管什么
这个 Demo 把 DJI Mobile SDK 的能力拆成三个工程模块,这个结构本身就是 SDK 官方建议的分层方式,理解它的边界对后面改代码特别重要。
android-sdk-v5-uxsdk 是 UI 组件库,名字里的 ux 是 user experience 的缩写。MapWidget.java、HSIMarkerLayer.java 这两个地图相关核心类都在这个模块里。它把飞行器的位置、航向、电池电量、图传信号等状态翻译成可视组件,业务代码只需要把组件注册进界面,不用自己从零画控件。你在这个模块里能看到大量自定义 View 和布局文件,2137 个文件里的 776 个 XML,一大半属于这里,布局和样式配置都集中在 res/layout 与 res/values 下。
android-sdk-v5-as 是 Auto Flight 能力模块,航线规划、航点任务、虚拟围栏、返航逻辑这类偏算法和状态机的东西在 as 模块下。它的特点是 UI 依赖极少,核心是飞行控制相关的逻辑封装。如果你的应用不需要地图界面、只需要飞行动作能力,可以只依赖 as 加 SDK 本体,省掉 uxsdk 的大量 UI 依赖,包体积能明显降下来。这一点在二次开发选型时很实用。
android-sdk-v5-sample 是示例应用壳,同时依赖前两个模块,把 SDK 能力串起来形成可运行的 APK。对新手来说 sample 是入口,对做二次开发的人来说 sample 反而是"主战场":你要接自己业务流程的地方通常在 sample,而你要改 SDK 默认行为的地方通常在 uxsdk 和 as。定位问题时按 sample → 被调用模块的顺序查,能省掉一半翻文件的时间。
模块依赖方向是单向的:sample 依赖 uxsdk 和 as,as 不依赖 uxsdk。这个约束保证了工程里不会出现循环依赖。你自己新增模块时也要保持这个方向,不然 Gradle 会报 Circular dependency 错误,而且这个错误出现时往往藏着牵一发动全身的引用调整。
每个模块目录下有自己的 build.gradle,但依赖版本不是各写各的,而是集中在根目录的 dependencies.gradle 里统一管理,下一节单独讲。
2.3 dependencies.gradle 与 build.gradle:版本集中管理与依赖引用
大工程最怕版本号散落各处,这个 Demo 用 dependencies.gradle 做集中管理。根目录的 dependencies.gradle 用 ext 块声明了一组版本变量:
// dependencies.gradle —— 全局版本中心 ext { sdkVersion = "5.x.y" // 按你实际接入的 SDK 版本填写 mapboxSdkVersion = "10.x" // 与 sdkVersion 配套,见 5.1 appCompatVersion = "1.6.x" kotlinVersion = "1.9.x" // 与 Android Studio 插件版本兼容 gsonVersion = "2.10.x" }各子模块 build.gradle 里不写死版本,而是通过变量引用:
// android-sdk-v5-sample/build.gradle —— 依赖集中引用 dependencies { implementation project(':android-sdk-v5-uxsdk') implementation project(':android-sdk-v5-as') implementation "androidx.appcompat:appcompat:${appCompatVersion}" implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}" implementation "com.google.code.gson:gson:${gsonVersion}" implementation "com.mapbox.mapboxsdk:mapbox-android-sdk:${mapboxSdkVersion}" }逻辑说明:ext 块是 Gradle 的额外属性容器,子模块构建脚本通过${变量名}左插值读取。这种写法的价值在于升级依赖时只改 dependencies.gradle 一个文件,而不是翻遍所有模块的 build.gradle。这里有个隐蔽的前置条件:根工程的 build.gradle 顶部必须先执行apply from: "dependencies.gradle",否则子模块解析时找不到这些属性,报错形如Could not get unknown property 'mapboxSdkVersion'。我见过有人只拷走了某个子模块的 build.gradle、忘了拷根工程配置,结果整个下午都在和这个报错搏斗,最后发现是属性没加载。
版本耦合是这个工程里最需要留意的点。DJI Mobile SDK 的 V5 系列和 Mapbox SDK 存在配套关系,不是任意组合都能跑。在 dependencies.gradle 里把 sdkVersion 升上去、但 mapboxSdkVersion 保持旧值,地图组件很容易在真机启动阶段崩掉,而且是 native 层崩溃,logcat 里只有几个 so 库的堆栈,看着完全无从下手。避坑办法是升级前先查对应 SDK 版本的发行说明,把 Mapbox 版本同步对齐,再动业务代码。这条经验在第 5 章会再展开一次。
到这一步,构建链的完整脉络已经清楚了:settings.gradle 管模块和仓库,dependencies.gradle 管版本,build.gradle 管组装,gradlew 管执行。顺序和层级理清,后面读代码才不容易迷路。
3. 地图组件实战:MapWidget 与 HSIMarkerLayer 的联动逻辑
3.1 MapWidget:把无人机状态绑到地图控件上
文件列表里的 MapWidget.java 属于 android-sdk-v5-uxsdk 模块,它是整个 Demo 里和地图关系最密切的组件。先明确它的定位:它不是简单包装一个地图 SDK,而是"飞行器状态与地图之间的桥梁"。MapWidget 内部组合了一个地图实例,同时监听飞行控制器的状态回调,飞行器的经纬度、高度、航向变化时,MapWidget 自动更新飞机在图上的位置和姿态角。
初始化代码通常会写在 Activity 的 onCreate 里:
// MapWidget 初始化:异步加载地图引擎 MapWidget mapWidget = findViewById(R.id.map_widget); mapWidget.initMap(new OnMapReadyCallback() { @Override public void onMapReady() { // 地图加载完成后才能设置参数 mapWidget.setMapType(MapWidget.MapType.SATELLITE); mapWidget.setMyLocationEnabled(true); mapWidget.setLogoVisibility(false); } });逻辑说明:initMap 是异步过程,onMapReady 回调触发才代表地图引擎加载完成。三个设置项里,setMapType 切换底图样式,卫星图在户外飞行场景下辨识度明显更高;setMyLocationEnabled 控制显示移动端自身的定位点,调试时可以用来确认设备 GPS 是否已在工作;setLogoVisibility 控制地图厂商 logo 的显隐,正式发布版本建议保留,以符合地图服务的条款要求。
注意,onMapReady 没触发之前,任何对地图视图对象的操作都会拿到无效引用。常见翻车写法是在 onCreate 里紧跟 initMap 之后立刻调用 setMapType,此时地图引擎还在初始化,轻则设置不生效,重则空指针崩溃。这类问题在真机上表现不稳定,有时候能跑有时候秒退,属于典型的"时序玄学"。
MapWidget 的常用方法可以归成一张参数表:
| 方法 | 作用 | 参数说明 |
|---|---|---|
| setMapType(MapType) | 切换底图样式 | SATELLITE / NORMAL / HYBRID |
| setMyLocationEnabled(boolean) | 显示设备定位点 | true 打开,false 关闭 |
| setLogoVisibility(boolean) | 控制地图 logo 显隐 | 发布版建议保留 |
| addMarkerLayer(Layer) | 叠加自定义图层 | 接受 HSIMarkerLayer 等图层实例 |
| setCameraAnimationsEnabled(boolean) | 控制相机动画开关 | 频繁跟随飞行器时建议关闭 |
参数表里最后一行是实践里容易忽略的:飞行器高速飞行时,相机跟随动画如果每帧都触发平滑移动,地图渲染线程会被拖垮,表现为画面掉帧严重。做航线巡检类应用时,我一般会关掉相机动画,改成按固定时间间隔跳变定位,视觉上更稳。
3.2 HSIMarkerLayer:仿 HSI 仪表盘的航向标记层
HSIMarkerLayer.java 是 uxsdk 模块里的自定义图层,名字里的 HSI 来自民航仪表概念 Horizontal Situation Indicator(水平状态指示器)。在无人机场景里,它把飞行器当前航向、目标航向、航向偏差画成一个复合标记,叠加在 MapWidget 的地图上。简单说,它就是给地图加了一个仿飞行仪表盘的航向指示器。
初始化与更新分两步走:
// Step 1:创建图层实例并添加到 MapWidget HSIMarkerLayer hsiLayer = new HSIMarkerLayer(context); mapWidget.addMarkerLayer(hsiLayer); // Step 2:飞行器航向变化时更新图层 flightController.getCompass().setStateCallback(new CompassStateCallback() { @Override public void onUpdate(@NonNull CompassState compassState) { hsiLayer.updateHeading(compassState.getHeading()); hsiLayer.invalidateContent(); // 标记图层需要重绘 } });逻辑说明:addMarkerLayer 把 HSI 图层挂到地图上。之后每次航向变化,CompassStateCallback 拿到当前罗盘角度,updateHeading 更新图层内部角度值,invalidateContent 通知渲染层重绘。这个流程的核心是回调驱动,而不是轮询——轮询会浪费大量 CPU 在无意义的取数上。如果你的业务还要叠加目标航点方向,可以在 updateHeading 之外再调一个 setTargetHeading 方法,两个角度的差值越大,HSI 指示器的偏转就越明显,飞手扫一眼就能判断当前飞机偏了多少度。
参数说明:heading 的取值范围是 0 到 360,单位是度。拿到原始值后建议做一次归一化处理,把 360 带回 0:有些飞控固件在特定转向动作里会偶发返回 360 而不是 0,直接用会导致标记突然跳转一整圈,视觉上像抖动。项目里很多飞行器状态数值都有类似的小脾气,写工具方法做边界清洗是个好习惯。
HSIMarkerLayer 绘图本身不复杂,但它依赖的坐标系转换绕不开:飞行器的 GPS 经纬度要先投影到地图平面坐标,再把航向角度映射成屏幕上的旋转角。这个转换在 SDK 内部已经封装,调用者不需要自己写投影公式,但要知道图层绘制是独立于地图底图的独立 Canvas,更新频率过高时会有绘制延迟。出现"飞机都拐弯了,HSI 标记还在缓慢转"的现象时,多半不是回调没触发,而是 invalidateContent 调用间隔太密,渲染线程来不及消化。
3.3 注册回调与生命周期:最常见的空指针位置
地图组件虽然用起来直观,但它强依赖 Activity 的生命周期管理。MapWidget 和 HSIMarkerLayer 的注册顺序可以归纳成一句话:先地图就绪、再挂图层、最后绑回调。顺序错了,大概率在某个隐蔽的时机点炸出空指针。
生命周期上必须做对称销毁:
@Override protected void onStart() { super.onStart(); mapWidget.onStart(); } @Override protected void onStop() { mapWidget.onStop(); super.onStop(); }逻辑说明:地图引擎内部有独立的渲染线程和资源池,onStart 和 onStop 通知是引擎恢复与释放资源的时机。漏掉 onStop 会带来两个问题:Activity 不可见时地图仍在渲染,耗电明显;切后台再回前台时,偶发出现地图黑屏。这是 DJI Mobile SDK 示例代码里反复出现的标准写法,我建议直接固化到自己的 Activity 基类里,避免每个页面各写一套、漏掉一半。
还有一个高频崩溃点:在 onMapReady 回调之前调了 addMarkerLayer,或者在地图销毁之后还持有 HSIMarkerLayer 引用去刷新。前者的报错通常指向空指针,后者的报错指向 IllegalStateException。根因都是没把"地图生命周期"和"图层生命周期"绑定。规避方法是在 HSIMarkerLayer 外面包一层管理器,由它统一响应 onMapReady、onStart、onStop 三个时机,业务层只跟管理器打交道,不直接碰图层对象。
另一个容易踩的性能问题:多个页面复用同一个 MapWidget 实例时,如果在 onStop 里没有把图层引用清掉,再次进入页面时 addMarkerLayer 会叠加多个 HSI 图层,画面出现重影。这个 bug 的排查周期往往很长,因为不是必现,只在页面反复进出后才出现。真遇到了,先把 addMarkerLayer 的调用次数打点,数一数是不是每次进页面都多加了一层。
4. Java 与 Kotlin 混编的落地姿势:460 个 Java 与 396 个 Kotlin 的分工逻辑
4.1 分层边界:Java 负责核心链路,Kotlin 负责界面与数据类
这个 Demo 最值得学习的不是某个控件怎么用,而是双语言混编的工程组织方式。460 个 Java 文件集中在 SDK 底层封装和业务逻辑链路,396 个 Kotlin 文件集中在 UI 层、数据模型和工具类。
为什么这样分?看 DJI Mobile SDK 的接口形态就能明白:几个核心控制类的接口是 Java 风格的,Java 的可空性语义和 SDK 内部实现最匹配,底层控制链路用 Java 写,能减少语言特性带来的类型转换噪音。到了 UI 层,Kotlin 的扩展函数、协程、空安全特性能让界面代码短一截,data class 定义回调数据模型也比手写 getter/setter 高效得多。这个 Demo 里常见的混合模式是:Kotlin UI 层单向调用 Java 业务层,Java 业务层不反向依赖 Kotlin,依赖方向一目了然。
如果你要把这段混编经验搬到自己工程,有一个经验值:核心控制类用 Java,页面和 ViewModel 用 Kotlin,数据模型看团队熟练度二选一。两头混着写逻辑不是不行,但维护时认代码的成本会高很多。Kotlin 和 Java 之间互调虽然零成本,但"零成本"指的是编译器层面的支持,不包含读者脑内切换语言的开销。
4.2 空安全约定:Kotlin 调用 Java 的平台类型陷阱
混编最隐蔽的坑是空安全语义不一致。Java 方法声明返回一个对象,Kotlin 无法确定它是否可空,于是把它当成"平台类型"。平台类型在语法上不强制判空,但运行时一旦为 null,崩溃来得毫无预兆。典型场景是 SDK 接口返回的飞行器控制对象:
// Kotlin 调用 Java 接口:平台类型的显式可空处理 val controller: FlightController? = sdkManager.getFlightController() controller?.startTakeoff { error -> // error 来自 Java 回调,也是平台类型,显式判空 if (error != null) { // 处理起飞失败原因 } }逻辑说明:getFlightController() 是 Java 方法,Kotlin 把它推断为平台类型。上面的示例把返回值声明成了显式可空类型 FlightController?,后续调用全部走 ?. 安全调用链,逻辑就锁死了,不会在某个回调分支漏掉判空。我见过线上事故就是这里偷懒:写代码时编译器没报警,真机上售后返厂的那台设备恰好拿不到 controller,空指针崩溃直接打在用户脸上。
另一个常见问题是静态成员的访问方式。Java 类里的public static final int STATUS_OK转成 Kotlin 调用时,经常被自动转换成Companion相关的伴生访问,绕了一层还不直观。更省事的姿势是在 Java 类上添加 @JvmStatic 注解,把静态方法暴露成 Kotlin 可以直接点用的形态:
// Java 侧:静态字段与静态方法定义 public class FlightStatus { public static final int STATUS_OK = 0; @JvmStatic public static boolean isOk(int status) { return status == STATUS_OK; } }// Kotlin 侧:像调顶层函数一样调用静态方法 if (FlightStatus.isOk(statusCode)) { // 状态正常的处理逻辑 }参数说明:@JvmStatic 的作用是让 JVM 为方法生成静态解析版本,这样 Kotlin 侧可以直接写 FlightStatus.isOk(...),不用绕道 FlightStatus.Companion.isOk(...)。注意这个注解只能加在 Java 类定义处。如果你不方便改动 SDK 里的类,就只能在自己的 Kotlin 代码里用 companion object 做一层中转封装,把静态成员重新暴露成干净的接口。
4.3 资源文件定位:776 个 XML、图片与音频的组织规律
资源文件占了工程总量的一大半,看不懂组织规律的人在改 UI 时会非常痛苦。776 个 XML 里,布局文件集中在各模块的 res/layout 下,命名规律是"页面或控件名 + 后缀",比如 activity_main.xml、map_widget_layout.xml。另一个重要 XML 是 AndroidManifest.xml,权限声明、App Key、Activity 注册都在里面,改包名和权限必须动它。
图片资源里,327 个 PNG 和 138 个 webp 按密度分桶放在 drawable 系列目录,这是 Android 的标准做法。云台图标、电池电量的状态图通常会在多套像素密度下出现多份,替换 UI 素材时至少要覆盖 drawable-xxhdpi 和 drawable-xxxhdpi 两档,否则部分机型上图标边缘会糊。这里有一个资源定位的实用技巧:在 Android Studio 的 Project 视图下,先按模块看 res,再按资源类型看子目录,最后按名称全局搜索。三层定位效率最高。
音频方面,14 个 MP3 加 3 个 WAV 集中在 res/raw 下,多是提示音和告警音。Java 或 Kotlin 里用 R.raw.xxx 引用,属于 Android 资源系统标准路径。替换音频时有个高频坑:MediaPlayer 播放 WAV 和 MP3 的解码方式不同,直接把 MP3 文件改名成 .wav 放进资源目录,播放器初始化会失败或者播放出杂音。编码格式和扩展名必须一致。这类问题在资源替换时几乎必然会遇到一次。
至于 4 个 JSON 和 3 个 properties 文件,属于配置数据。JSON 多见于模拟数据或配置文件,properties 多半是文案或 SDK 参数配置。它们不参与编译,打包时作为原始文件进入 APK,运行期用流读取。改这些文件不需要重新编译,但要把它们从 APK 里取出来得用解包工具,调试时建议在代码里打印读取结果,直接确认运行期命中。
5. 避坑指南:编译、注册、真机调试里的五条翻车记录
5.1 编译期:Gradle 发行版下载失败与 Mapbox 版本不自洽
现象 1:第一次执行 gradlew,终端长时间停在Downloading gradle-...zip,最终报连接超时或重置。原因很直接:wrapper 指向的 Gradle 发行版在压缩包下载阶段被网络环境中断了。这完全不是代码问题,但在新手看来和代码报错没有区别,容易误判。解决方法是手动下载同名发行版压缩包,放进~/.gradle/wrapper/dists/gradle-.../<hash>/目录并解压,再重新执行 gradlew。Linux 和 macOS 路径在用户目录下的 .gradle 里,Windows 在C:\Users\<用户名>\.gradle下。放对位置后 wrapper 不会再触发下载,构建直接走到项目编译阶段。
现象 2:业务代码一行没改,只动了依赖版本,编译通过,装到真机上启动几十秒后地图区域黑屏或直接闪退,logcat 里全是 native 层 so 库堆栈。根因是 SDK 版本和 Mapbox 版本不配套,常见于只升级了 SDK 而 Mapbox 保持旧值。SDK 内部按约定版本调用 Mapbox 接口,版本差太多时 native 层对不上。解决办法是升级前先查 SDK 发行说明里的依赖对照表,把 dependencies.gradle 里的 mapboxSdkVersion 对齐,然后执行./gradlew clean做一次全量构建,别用增量编译,增量编译可能残留旧依赖的 class 文件。
5.2 运行期:App Key 校验失败与功能按钮置灰
现象 3:App 启动后立刻弹注册失败,提示指向 App Key 校验不通过。原因基本是三个:Key 没填、填错位置、包名与申请 Key 时填写的包名不一致。SDK 校验 Key 时会同时校验包名,任何一个不对等都通过不了。解决方法是先查 AndroidManifest.xml 里 application 节点下的 meta-data 配置,确认包名和申请 Key 时的完全一致,再确认 Debug 和 Release 签名。本地调试强烈建议使用申请 Key 时绑定的证书签名,换签名会导致校验失败,这是最容易忽视的一条。
现象 4:飞机连接成功,App 内部分功能按钮灰色不可点,点击没有任何响应,SDK 日志里能看到 Command not supported。原因是 SDK 版本与固件版本不匹配,飞机固件太旧,SDK 查询能力集后把硬件不支持的功能置灰。不要为了迁就旧固件去降 SDK 版本,那样很可能把地图组件也带回旧行为,两头受气。正确做法是通过官方调参工具把固件升到与 SDK 匹配的版本,升级后重新连接,按钮恢复可点。
5.3 真机调试:模拟器失效与地图回调不触发
现象 5:在 Android 模拟器里装好 App,连不上任何飞行器模拟信号,点起飞直接报错。原因是 DJI Mobile SDK 的 V5 版本对模拟器支持非常有限,图传链路和部分传感器接口依赖真机硬件节点,标准模拟器上没有这些设备。这个工程的定位决定了模拟器只能看 UI 布局,跑不通飞行链路是预期行为,不是你的代码 bug。真机调试时建议用高质量 USB 数据线,插主机后置接口,图传画面偶发卡顿先排除物理链路问题,再排查无线干扰,顺序反了会白折腾半天。
现象 6:onMapReady 回调一直不触发,地图区域始终空白。原因是地图引擎的访问令牌缺失,或设备网络访问不了地图服务的地址。Demo 里内置的令牌换到你自己的工程后如果没有正确配置,地图加载会静默失败,不报错也不弹提示。解决方法是检查地图平台的密钥是否配置到 AndroidManifest.xml 的 meta-data 里,同时确认设备能访问地图服务域名。用调试工具抓一层 HTTPS 请求,一两分钟就能确认是不是网络问题,不用在代码里瞎找。
6. 把 Demo 改造成自己的项目:四步替换法
拿到这种体量的 Demo,不建议从头重写,也不建议逐行啃完。我习惯按四步替换法把它变成自己的工程底座,每完成一步就编译一次,确保问题被限制在可见范围内。
第一步,替换包名与签名。把 sample 模块的 applicationId 改成自己的域名反写,同步替换 AndroidManifest.xml 里注册的组件名。注意三兄弟里只有 sample 需要改 applicationId,uxsdk 和 as 是库模块,包名动它们反而会引起内部资源引用错乱。改完跑一次 assembleDebug,验证构建链还活着。
第二步,替换密钥配置。把自己的地图访问令牌填进 meta-data,把 App Key 换成自己在开发者后台申请的值,删掉 Demo 自带的示例令牌。这一步做完,真机上 App 启动应该不再弹注册失败,地图也能正常加载。
第三步,按需裁剪模块。目标应用只有航线能力、不需要地图界面时,从 sample 的 build.gradle 里拿掉 uxsdk 依赖;只要 UI 展示、不要自动飞行时,拿掉 as。裁剪后要回到 sample 代码里删除引用被剪模块的类,否则编译期报 unresolved reference,一处漏删全编译失败。裁剪是四步里最容易翻车的一步,务必每删一个依赖就编译一次。
第四步,替换业务 UI 与音频资源。把 drawable 下的图片换成自己的素材,音频文件注意编码格式与扩展名一致,一次性替换干净。做完这步工程就已经是你的壳了,剩下就是往 sample 的业务代码里填自己的逻辑。
我自己的习惯是每完成一步就编译一次,四步走完工程基本成形。之前图快,一次性改包名、改资源、剪模块,结果编译报错叠了十几条,根本分不清是包名问题还是裁剪问题,白白耗掉一整天。从那以后我每次接手同类 SDK Demo 都强制自己走完四步、逐步验证,再复杂的工程也能稳住节奏。希望帮到你。
本文还有配套的精品资源,点击获取