Android上位机应用开发方案(Qt转安卓)

6 阅读16分钟

这是一份AI技术方案,将我现有的Qt应用转为Android应用。

一、项目概述

项目要素说明
目标将现有Qt上位机软件转为原生Android应用
开发语言Kotlin(Java开发者可无缝上手)
最低SDKAPI 28 (Android 9.0) —— 经重新评估后的正式起点
目标SDKAPI 36 (Android 16)
适配要求手机和平板自适应,支持横屏/竖屏切换
网络通信局域网WiFi通信(Socket/NSD)
核心数据SQLite持久化 + 内存缓存(多雷达阵面管理)
核心约束低成本、高稳定、易维护

二、Android版本支持策略(关键决策)

2.1 最终确定的最低支持版本

经过全面评估,最低正式支持版本定为 Android 9.0 (API 28),不再支持Android 8及以下版本。

Android 版本API Level支持策略决策理由
Android 5.0API 21❌ 不支持市场份额已极低(<1%),维护成本高
Android 6.0API 23❌ 不支持运行时权限模型老旧,兼容性投入产出比低
Android 7.0API 24❌ 不作为正式目标2026年7月起Google Play服务最低要求已升至API 24,继续支持存在底层服务风险
Android 8.0API 26⚠️ 不建议开发框架趋势明确向上,维护成本高
Android 9.0API 28✅ 最低正式支持稳定存量设备,避开API 21-26的维护泥潭
Android 10~12API 29~31✅ 重点兼容用户基数庞大,测试性价比最高
Android 13~15API 33~35✅ 重点兼容当前及未来的主流版本
Android 16API 36✅ 当前目标2026年最新版本,需关注大屏强制适配
Android 17API 37✅ 前瞻适配长期迭代目标,确保新设备兼容

2.2 版本策略的核心理由

  1. 生态系统的自然淘汰:2026年7月,Google Play服务最低要求已提升至Android 7.0 (API 24)。继续支持更老版本,设备可能因缺少底层服务更新而无法正常运行应用。
  2. 开发框架的跟进趋势:主流开发框架正加速放弃对Android 8及更早版本的支持。选择API 28作为起点,能在未来更长时间内使用最新的稳定版开发库。
  3. "低成本稳定"的真实含义:支持Android 5-8意味着投入额外时间处理老旧系统特有的Bug和兼容性问题。放弃市场份额已极低的旧系统,将精力集中在主流版本上,反而是更经济、更稳健的策略

2.3 如确有特殊需求需覆盖极老设备

若因特殊场景(如军工、特定工业现场)必须支持Android 5-8设备,建议采取以下策略:

  • 从Google Play的兼容设备列表中移除这些设备
  • 单独维护一个功能精简的旧版本分支(不推荐长期并行维护)

三、核心决策对比:Qt移植 vs. Kotlin原生重写

对比维度Qt 5.12 移植Kotlin 原生重写
环境配置复杂,5.12版本存在已知Bug和SSL兼容问题Android Studio一键配置
UI适配Qt Widgets灵活性差,平板/手机适配极难ConstraintLayout成熟方案
技术门槛需同时熟悉C++/Qt/Android纯Android生态,你已有Java+原生安卓基础
维护成本高,找问题和修复都困难低,社区支持强大
预估工时不可控(环境问题可能耗时数周)约10-14天

结论:采用Kotlin原生重写方案

四、技术选型

组件选型方案理由
开发语言Kotlin官方推荐,与Java完全互通,代码更简洁
UI框架传统View系统 + ConstraintLayout兼容性最好(支持API 7+),适配灵活
网络通信java.net.Socket + NsdManager你已精通Java Socket,零学习成本
数据库Room (SQLite)官方ORM库,编译期SQL校验,与Kotlin协程完美配合
内存缓存StateFlow / MutableStateFlow响应式数据流,UI自动感应数据变化
异步处理Kotlin Coroutines(协程)轻量级并发,替代AsyncTask和Thread
位置服务FusedLocationProviderClient + SensorManager获取设备经纬度与航向,官方推荐API
依赖注入暂不引入中小项目手动管理更简单可控
最低SDKAPI 28 (Android 9.0)经评估后的稳定起点
目标SDKAPI 36 (Android 16)2026年最新版本

五、开发路线图(预计10-14天)

阶段任务内容预估工时关键要点
阶段一:环境搭建安装Android Studio,配置JDK 17+,创建项目0.5天设置minSdk=28,targetSdk=36
阶段二:语言过渡用Kotlin写Java风格代码,熟悉基本语法差异1天重点:val/var、空安全?when表达式、类声明:
阶段三:UI布局开发使用ConstraintLayout构建主界面,适配手机和平板3-4天使用约束比例、屏障(Barrier)、组(Group),一套布局适配所有屏幕
阶段四:数据库与缓存集成Room,设计RadarSite实体和DAO,实现Repository缓存层2天内存缓存(StateFlow) + SQLite持久化,启动时加载到内存
阶段五:雷达态势图自定义View + Canvas绘制PPI雷达图,支持目标实时更新2-3天坐标系转换,性能优化,硬件加速适配
阶段六:网络通信移植Java Socket逻辑,集成NsdManager设备发现2-3天纯Java网络代码可直接复用
阶段七:位置与传感器集成FusedLocationProviderClient获取经纬度,SensorManager获取航向1-2天运行时权限申请,前台服务类型声明
阶段八:Android特性适配运行时权限(API 23+)、前台服务(API 26+)已包含minSdk=28后,需处理的兼容性问题大幅减少
阶段九:横屏适配声明resizeableActivity,测试横竖屏切换UI1天Android 16+大屏强制适配需提前准备
阶段十:测试与发布多分辨率/安卓版本模拟器及真机测试2-3天重点测试API 28~36各版本,覆盖手机和平板

六、硬件配置要求

6.1 最低与推荐配置

配置项最低要求推荐配置说明
操作系统Android 9.0 (API 28)最新稳定版已确定的minSdk,所有新API和性能优化的基础
处理器(CPU)1GHz 双核八核 2.0GHz 以上更高的主频和核心数确保态势图动画平滑
运行内存(RAM)1GB4GB 或以上多阵面配置 + 态势图缓存 + 系统开销,需充足余量
存储空间100MB 可用空间256GB 或以上应用本体及本地SQLite数据库占用不大
屏幕分辨率800x4801920x1080 或更高自适应布局可兼容各类分辨率
传感器陀螺仪 + 加速度计 + 磁力计支持用于获取设备航向信息(获取经纬度无需特定传感器)
定位模块GPS / 网络定位GPS + GLONASS + 北斗用于获取设备经纬度

6.2 内存详细分析

应用内存占用估算(正常运行状态)

内存占用项估算大小说明
Android 9 系统基础占用~600-800MB不同设备定制系统差异较大
应用本身 (APK + 运行时代码)~30-50MB未混淆前
雷达态势图 (Canvas绘制缓冲区)~10-20MB取决于屏幕分辨率
多阵面缓存数据 (假设20个阵面)~1-2MB每个阵面配置约几十KB
网络数据缓冲~5-10MBSocket收发缓冲区
传感器数据流~1-2MB持续更新的传感器数据
其他 (UI缓存、临时对象等)~20-50MBGC可回收
总计(最低)~700-900MB1GB内存设备处于临界状态
总计(推荐)~150-250MB(应用自身)4GB内存设备游刃有余

内存优化策略

  • 态势图绘制优化:使用对象池复用PaintPath,减少onDraw()中临时对象分配
  • 数据缓存策略:仅缓存当前可见阵面的数据,不可见阵面数据从SQLite延迟加载
  • 图片资源:使用WebP格式压缩图片资源,减少APK体积和运行时内存占用
  • 内存抖动控制:高频网络数据更新时,避免频繁创建新对象触发GC

6.3 设备传感器与定位要求

方案一:获取设备经纬度(FusedLocationProviderClient)

使用Google Play服务的FusedLocationProviderClient,自动选择最优定位方式。

// 依赖:implementation 'com.google.android.gms:play-services-location:21.0.1'

// AndroidManifest.xml 权限声明
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- 如需后台获取位置 -->
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

// 运行时权限动态申请(Android 6.0+)
// 获取最后已知位置
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
    val latitude = location?.latitude ?: 0.0
    val longitude = location?.longitude ?: 0.0
    // 使用经纬度
}

// 如需实时位置更新
fusedLocationClient.requestLocationUpdates(
    LocationRequest.create().apply {
        priority = LocationRequest.PRIORITY_HIGH_ACCURACY
        interval = 1000 // 更新间隔(毫秒)
    },
    locationCallback,
    Looper.getMainLooper()
)

方案二:获取设备航向(SensorManager)

使用SensorManager获取设备朝向(方位角/航向角)。

// 注册旋转矢量传感器(融合陀螺仪+磁力计,输出平滑稳定)
sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)?.let {
    sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_UI)
}

// 在 onSensorChanged 中计算航向
override fun onSensorChanged(event: SensorEvent?) {
    event?.let {
        if (it.sensor.type == Sensor.TYPE_ROTATION_VECTOR) {
            SensorManager.getRotationMatrixFromVector(rotationMatrix, it.values)
            SensorManager.remapCoordinateSystem(rotationMatrix, 
                SensorManager.AXIS_X, SensorManager.AXIS_Z, rotationMatrix)
            SensorManager.getOrientation(rotationMatrix, orientationValues)
            val azimuth = Math.toDegrees(orientationValues[0].toDouble()).toFloat()
            // azimuth 范围 -180° ~ 180°,可归一化为 0° ~ 359°
            val heading = (azimuth + 360) % 360
            // 下发航向给设备
        }
    }
}

传感器硬件要求

传感器类型用途是否必需替代方案
GPS/定位模块获取经纬度必需无替代,必须硬件支持
陀螺仪 (Gyroscope)航向计算(融合算法)⚠️ 强烈建议仅磁力计航向易受干扰,精度较差
磁力计 (Magnetometer)航向计算(地磁方向)⚠️ 强烈建议与陀螺仪配合使用效果最佳
加速度计 (Accelerometer)设备姿态检测⚠️ 建议提升航向计算稳定性

注意事项

  • 缺少陀螺仪/磁力计的设备仍可获取航向,但精度会显著下降(仅靠GPS速度方向推算)
  • Android 10 (API 29) 及以上要求前台服务声明foregroundServiceType="location"才能在后台持续获取位置
  • Android 12 (API 31) 及以上要求动态申请ACCESS_BACKGROUND_LOCATION权限时,需引导用户到设置页面手动开启
  • 位置信息是敏感隐私数据,需遵守GDPR等数据保护法规要求

6.4 设备选购建议(针对硬件采购场景)

场景推荐配置典型设备示例
最低成本测试Android 9, 2GB RAM, 有GPS二手小米/红米低端机
稳定运行Android 10-12, 4GB RAM, GPS+陀螺仪+磁力计荣耀/OPPO/vivo 中端机
主控平板(大屏展示)Android 11+, 6GB RAM, 10寸以上屏幕华为MatePad、联想小新Pad
高性能工业终端Android 13+, 8GB RAM, 三防+高精度定位亿道/研华工业平板

七、核心技术实现详解

7.1 屏幕自适应(手机/平板)

使用ConstraintLayout实现一套布局适配所有屏幕:

<!-- 示例:按钮宽度占屏幕30%,图片保持16:9比例 -->
<Button
    android:layout_width="0dp"
    app:layout_constraintWidth_percent="0.3"
    android:layout_height="wrap_content" />

<ImageView
    android:layout_width="0dp"
    app:layout_constraintDimensionRatio="H,16:9"
    android:layout_height="0dp" />

核心原则

  • 优先调整ConstraintLayout的约束关系,而非创建多套布局文件
  • 使用屏障(Barrier)和组(Group)处理动态内容变化
  • 使用Android Studio可调整大小模拟器实时预览

7.2 横屏/竖屏支持

<!-- AndroidManifest.xml -->
<activity
    android:name=".MainActivity"
    android:resizeableActivity="true"
    android:screenOrientation="fullSensor" />

⚠️ 重要提醒

  • Android 16起,大屏设备(最小宽度≥600dp)将忽略screenOrientation锁定
  • Android 16提供"选择退出"选项作为临时过渡
  • Android 17将彻底移除该选项,所有应用必须自适应

7.3 雷达态势图(PPI显示)

这是Qt上位机中最核心的视觉组件,可完整迁移到Android自定义View。

实现方案选择

方案实现方式适用场景性能开发成本
自定义View + Canvas重写onDraw()绘制目标点<500个,刷新率25-50fps优(CPU绘制)
OpenGL ESGLSurfaceView + GPU渲染目标点>1000个,高性能需求卓越
WebView + JS图表ECharts/Canvas绘制对移植成本敏感一般

推荐采用方案一:自定义View + Canvas,与你的"低成本稳定"目标最匹配。

核心绘制逻辑参考

class RadarPPIView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null
) : View(context, attrs) {

    private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
    private val targetPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        style = Paint.Style.FILL
    }
    
    private var targets = listOf<RadarTarget>()
    private var centerX = 0f
    private var centerY = 0f
    private var radius = 0f

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        centerX = w / 2f
        centerY = h / 2f
        radius = min(w, h) / 2f - 20f
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        drawRangeRings(canvas)      // 距离环
        drawRadialLines(canvas)     // 方位刻度线
        drawTargets(canvas)         // 目标点
        drawCrosshair(canvas)       // 中心十字
    }

    private fun drawTargets(canvas: Canvas) {
        targets.forEach { target ->
            // 极坐标转屏幕坐标(注意Y轴方向取反)
            val angleRad = Math.toRadians(target.azimuth.toDouble())
            val x = centerX + target.distance / maxRange * radius * sin(angleRad).toFloat()
            val y = centerY - target.distance / maxRange * radius * cos(angleRad).toFloat()
            
            targetPaint.color = when(target.category) {
                "UAV" -> Color.YELLOW
                else -> Color.RED
            }
            canvas.drawCircle(x, y, 8f, targetPaint)
        }
    }

    fun updateTargets(newTargets: List<RadarTarget>) {
        targets = newTargets
        postInvalidate() // 请求重绘
    }
}

Qt绘制与Android Canvas对照表

Qt绘制概念Android Canvas等效
QPainterCanvas
QPenPaint (stroke)
QBrushPaint (fill)
drawEllipse()canvas.drawCircle()
drawLine()canvas.drawLine()
drawText()canvas.drawText()

7.4 多雷达阵面管理(SQLite + 内存缓存)

每个IP地址对应一个雷达阵面,需要同时支持持久化存储和高效内存访问。

数据模型

@Entity(tableName = "radar_sites")
data class RadarSite(
    @PrimaryKey(autoGenerate = true)
    val id: Long = 0,
    val name: String,              // 阵面名称
    val ipAddress: String,         // IP地址(业务唯一标识)
    val port: Int,                 // 通信端口
    val latitude: Double = 0.0,    // 阵面部署纬度
    val longitude: Double = 0.0,   // 阵面部署经度
    val heading: Float = 0f,       // 阵面航向
    val isEnabled: Boolean = true,
    val lastOnlineTime: Long = 0,
    val customParams: String? = null
)

内存缓存层(Repository)

class RadarSiteRepository(private val dao: RadarSiteDao) {
    // 内存缓存:StateFlow作为可观察数据源
    private val _sitesFlow = MutableStateFlow<List<RadarSite>>(emptyList())
    val sitesFlow: StateFlow<List<RadarSite>> = _sitesFlow.asStateFlow()

    // 应用启动时加载数据库数据到内存
    suspend fun refreshCache() {
        _sitesFlow.value = dao.getAllSites()
    }

    // 增删改:先更新内存再持久化
    suspend fun addSite(site: RadarSite) {
        dao.insertSite(site)
        refreshCache()
    }

    suspend fun updateSite(site: RadarSite) {
        dao.updateSite(site)
        refreshCache()
    }

    suspend fun deleteSite(siteId: Long) {
        dao.deleteSiteById(siteId)
        refreshCache()
    }
}

7.5 位置与传感器集成

获取设备经纬度

// AndroidManifest.xml 权限声明
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

// 运行时权限申请(Android 6.0+)
if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) 
        != PackageManager.PERMISSION_GRANTED) {
    requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), REQ_CODE)
}

// 获取位置(FusedLocationProviderClient)
val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
    val latitude = location?.latitude ?: 0.0
    val longitude = location?.longitude ?: 0.0
    // 下发至设备或显示在UI
}

获取设备航向

// 使用旋转矢量传感器
sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)?.let {
    sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_UI)
}

override fun onSensorChanged(event: SensorEvent?) {
    event?.let {
        if (it.sensor.type == Sensor.TYPE_ROTATION_VECTOR) {
            SensorManager.getRotationMatrixFromVector(rotationMatrix, it.values)
            SensorManager.remapCoordinateSystem(rotationMatrix, 
                SensorManager.AXIS_X, SensorManager.AXIS_Z, rotationMatrix)
            SensorManager.getOrientation(rotationMatrix, orientationValues)
            val heading = (Math.toDegrees(orientationValues[0].toDouble()) + 360) % 360
            // 下发给设备
        }
    }
}

下发经纬度/航向给设备

class RadarCommunicationManager {
    fun sendPoseToDevice(ip: String, port: Int, 
                         latitude: Double, longitude: Double, heading: Float) {
        val message = buildPoseMessage(latitude, longitude, heading)
        sendData(ip, port, message)
    }
}

7.6 网络通信

// Java Socket代码可直接复用,Kotlin无缝调用
// 局域网设备发现使用NsdManager (Android 4.1+)
val nsdManager = getSystemService(Context.NSD_SERVICE) as NsdManager

注意事项

  • Android 9+默认禁止明文HTTP流量,需配置network_security_config.xml
  • 如需后台持续网络通信,使用前台服务(Foreground Service)

7.7 发热控制(ADPF自适应性能框架)

// 获取设备热状态,动态调整应用负载
val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager
val thermalHeadroom = powerManager.getThermalHeadroom(1000) // 未来1秒的热余量
// 根据thermalHeadroom值动态调整网络请求频率/UI刷新率

效果参考:知名游戏开发商Netmarble利用ADPF改善设备热余量11%,帧率更稳定。

八、Android 16 (2026) 及未来版本注意事项

变化点影响应对措施
大屏强制全屏应用在大屏设备上无法锁定方向提前声明resizeableActivity="true",UI自适应
16KB内存页支持API 35+设备强制要求确保应用兼容16KB页面大小(Google Play上架要求)
后台任务限制后台执行配额管理更严格优化后台网络任务,使用前台服务
私有API限制非公开SDK接口访问受限替换为官方公开API

九、老机型兼容策略(API 28+)

兼容点策略
最低API版本28 (Android 9.0)
UI兼容ConstraintLayout支持API 7+,无兼容性问题
网络兼容java.net.Socket标准库全版本支持
权限兼容统一使用Android 6.0+运行时权限模型
Room数据库支持API 14+,API 28无额外兼容负担
传感器兼容SensorManager API 1+全版本支持,旋转矢量传感器API 9+支持
位置服务FusedLocationProviderClient需Google Play服务,API 28+均支持
测试覆盖使用API 28~36各版本模拟器验证

十、维护与迭代建议

  1. 业务逻辑复用:Java网络协议解析、数据处理代码直接复制到新项目,零风险迁移
  2. 模块化设计:网络模块、数据模块(Repository+DAO)、UI模块、传感器模块分离
  3. 版本控制:建议使用Git,标记稳定版本便于回滚
  4. 监控埋点:关键网络请求、数据库操作、位置获取加入日志
  5. 版本升级策略:每年评估一次minSdk,若API 28设备市场份额降至阈值以下,可考虑提升

十一、工时与成本估算

项目估算
开发周期10-14天(单人从容开发)
测试周期3-5天(覆盖API 28~36各主要版本,手机+平板)
学习成本Kotlin语法1-2天过渡,其余技能已具备
维护成本低,纯Android生态,社区资源丰富
硬件成本无额外硬件投入,使用现有Android设备测试
数据库设计已包含在开发周期内,无额外成本
传感器集成已包含在开发周期内,无需额外硬件

十二、结论与建议

本方案充分利用多年Java功底和原生安卓经验,在不引入额外技术栈(Qt/C++)的前提下,以最低成本和风险交付高质量、易长期维护的原生Android应用。

关键成功因素

  • Android版本策略务实:以API 28为起点,避开旧版本维护泥潭,专注主流设备
  • 硬件要求明确:最低1GB RAM + GPS定位,推荐4GB RAM + 陀螺仪+磁力计
  • 位置与传感器方案成熟:FusedLocationProviderClient获取经纬度,SensorManager获取航向,均为Android官方推荐API
  • Kotlin不必专门学习:从Java风格写起,边写边熟悉
  • 网络代码完全复用:核心业务逻辑零风险迁移
  • UI适配有成熟方案:ConstraintLayout解决手机平板横竖屏所有问题
  • 雷达态势图完美迁移:自定义View + Canvas绘制,性能可控,与Qt绘制逻辑一一对应
  • 数据管理清晰高效:SQLite持久化 + StateFlow内存缓存,多阵面管理游刃有余
  • 发热问题可主动控制:ADPF框架提供官方解决方案
  • Android 16变化提前知晓:有充足时间应对过渡

预期交付质量:一个稳定运行在Android 9.0至Android 16全系列设备上、支持手机和平板自适应横竖屏切换、局域网WiFi通信、多雷达阵面本地化管理(SQLite持久化+内存缓存)、雷达态势图PPI显示、设备经纬度/航向获取与下发的原生Android应用。