简介:这是一份面向安卓开发初学者的实战学习项目,基于《一起来捉妖》游戏设计辅助定位与自动捉妖功能,聚焦移动应用逆向分析、自动化测试与虚拟定位技术实践。资源涵盖完整Android Studio工程,包含163张界面截图与图标资源(png/jpeg)、16个UI布局与权限配置文件(xml)、5个Native层动态库(so)、4个核心业务逻辑Java类、以及Airtest自动化脚本与腾讯定位SDK集成方案,包内共226个文件,总大小13.43MB。已有77人下载学习,适合希望掌握Android UI识别、WSS协议通信、虚拟定位调试及自动化交互流程的开发者。项目已实现屏幕鼓/妖识别、地图模拟行走、妖灵位置搜索与自动敲击捉取全流程,并附有gradle构建配置、第三方SDK依赖说明及开发者模式启用指引,结构清晰,模块可拆解复用。
1. “一起来捉妖辅助定位”不是外挂,而是安卓端地理围栏与传感器融合的典型学习项目
“一起来捉妖 辅助定位 自动捉妖 安卓开发学习 捉妖雷达.zip”这个标题在安卓开发者社区中高频出现,但它常被误读为“作弊工具”。实际上,它是一个面向初学者的、结构清晰的地理围栏(Geofence)+ 传感器融合(加速度计/磁力计)+ 地图 SDK 集成教学项目。核心目标是:在不调用任何非公开 API 或绕过游戏反作弊机制的前提下,通过监听设备位置变化、计算朝向角度、叠加虚拟雷达 UI,实现“附近妖怪热力提示”——本质是 Android 平台 LocationManager + SensorManager + Map SDK 的标准组合实践。适合已完成《Android 基础控件与 Activity 生命周期》学习、正进入“后台服务与传感器交互”阶段的开发者。项目 ZIP 包内通常含完整 Gradle 工程结构(settings.gradle、build.gradle)、已配置好高德或百度地图 Key 的AndroidManifest.xml,以及关键类RadarService.java和CompassView.kt。它不涉及 Hook、Xposed 或 root 权限,所有逻辑运行在应用沙盒内,符合 Google Play 及国内主流应用市场对位置类应用的合规要求。
2. 用 Gradle 构建链解析捉妖雷达的工程结构与依赖配置
一个可运行的“捉妖雷达”项目必须能通过./gradlew build成功生成 APK。这背后依赖于settings.gradle与build.gradle的精准协同。理解这两份文件的职责分工,是调试定位失败、地图白屏、传感器无响应等常见问题的第一步。
2.1 settings.gradle:声明模块拓扑关系,决定编译入口
settings.gradle是 Gradle 多模块项目的“地图索引”。对于典型的捉妖雷达项目,其内容通常如下:
include ':app' rootProject.name = "ZhuoYaoRadar"注意:若项目包含独立的
radar-core模块(用于封装地理围栏逻辑),此处必须显式声明include ':app', ':radar-core',否则app模块中implementation project(':radar-core')将报错Could not resolve project :radar-core。常见错误是开发者复制代码后未同步修改settings.gradle,导致gradlew build报Project with path ':xxx' could not be found。
该文件还可能包含仓库源配置(尤其在国内网络环境下):
pluginManagement { repositories { maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/google' } maven { url 'https://maven.aliyun.com/repository/central' } gradlePluginPortal() } }此配置确保com.android.tools.build:gradle等插件能从阿里云镜像拉取,避免因jcenter()关闭导致的构建中断。
2.2 app/build.gradle:定义雷达功能的依赖与权限契约
app/build.gradle是功能实现的“宪法”,它声明了雷达所需的所有能力边界。以下是关键配置段及其作用解析:
android { compileSdk 34 defaultConfig { applicationId "com.example.zhuoyao.radar" minSdk 21 targetSdk 33 // 注意:targetSdk 33 后需适配前台服务通知渠道 versionCode 1 versionName "1.0" // 必须声明,否则 Android 12+ 设备无法启动前台服务 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation 'androidx.core:core:1.12.0' // 提供 ActivityCompat 等兼容工具 implementation 'androidx.appcompat:appcompat:1.6.1' // 地图 SDK(以高德为例) implementation 'com.amap.api:map3d:9.7.0' implementation 'com.amap.api:location:6.2.0' // 独立定位 SDK,精度优于系统 LocationManager // 传感器与位置融合 implementation 'androidx.lifecycle:lifecycle-service:2.6.2' // 支持 ForegroundService implementation 'androidx.work:work-runtime-ktx:2.8.1' // 可选:用于后台任务调度 // 日志与调试 implementation 'androidx.logging:logging:1.2.3' }| 依赖项 | 作用 | 不可省略性 | 常见坑点 |
|---|---|---|---|
com.amap.api:location:6.2.0 | 提供AMapLocationClient,支持高精度 GPS/WiFi/基站混合定位,比LocationManager更稳定 | ★★★★★ | 若仅用map3d而漏掉location,onLocationChanged可能永不触发 |
androidx.lifecycle:lifecycle-service | 使RadarService继承LifecycleService,自动绑定生命周期,避免内存泄漏 | ★★★★☆ | 未引入时,startForeground()在 Android 12+ 上会抛ForegroundServiceDidNotStartInTimeException |
androidx.core:core:1.12.0 | 提供ActivityCompat.requestPermissions(),适配 Android 6.0+ 动态权限 | ★★★★★ | 缺失会导致ACCESS_FINE_LOCATION请求失败,日志显示Permission denied |
2.3 AndroidManifest.xml:将权限与服务注册为系统可识别的契约
AndroidManifest.xml是应用与 Android 系统的“正式协议”。捉妖雷达的核心服务与权限必须在此显式声明:
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- Android 12+ 必须 --> <uses-permission android:name="android.permission.VIBRATE" /> <application ...> <!-- 地图 SDK 初始化 --> <meta-data android:name="com.amap.api.v2.apikey" android:value="你的高德Key" /> <!-- 雷达核心服务 --> <service android:name=".service.RadarService" android:enabled="true" android:exported="false" android:foregroundServiceType="location|specialUse" /> <!-- Android 12+ 要求指定类型 --> <!-- 主 Activity --> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application>提示:
android:foregroundServiceType="location|specialUse"是 Android 12(API 31)强制要求。若只写location,在部分 OEM 定制系统(如华为 EMUI、小米 MIUI)上仍可能被系统杀死。specialUse是为雷达类持续定位场景预留的合法类型,需在AndroidManifest.xml中声明<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />并在RadarService.onCreate()中调用startForeground(1, notification)时传入ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE。
3. 实现捉妖雷达核心逻辑:地理围栏监听与朝向角实时计算
雷达效果的本质,是将设备物理朝向(罗盘角)与目标坐标方位角(bearing)做差值映射到 UI 圆盘上。这一过程需同时协调LocationManager(或高德AMapLocationClient)与SensorManager,并解决传感器数据抖动与坐标系转换两大难题。
3.1 高德定位 SDK 替代原生 LocationManager:提升定位稳定性
原生LocationManager在室内或弱信号下易返回陈旧坐标(getAccuracy() > 30)。高德AMapLocationClient提供更优的融合定位策略:
// RadarService.java private AMapLocationClient locationClient; private void initLocationClient() { locationClient = new AMapLocationClient(this.getApplicationContext()); AMapLocationClientOption option = new AMapLocationClientOption(); option.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy); // 高精度模式 option.setNeedAddress(true); option.setOnceLocation(false); // 持续定位 option.setWifiScan(true); locationClient.setLocationOption(option); locationClient.setLocationListener(this); // 实现 AMapLocationListener 接口 locationClient.startLocation(); }AMapLocationClient返回的AMapLocation对象包含getLatitude()、getLongitude()、getBearing()(设备移动方向角)及getAccuracy()。其中getBearing()在静止时不可靠,故雷达 UI 的“指针方向”应优先使用SensorManager计算的设备朝向角。
3.2 传感器融合:用加速度计与磁力计计算真实朝向角
单纯OrientationSensor已被弃用。正确做法是融合TYPE_ACCELEROMETER与TYPE_MAGNETIC_FIELD:
private float[] gravity = new float[3]; private float[] geomagnetic = new float[3]; private float[] rotationMatrix = new float[9]; private float[] orientation = new float[3]; private final SensorEventListener sensorListener = new SensorEventListener() { @Override public void onSensorChanged(SensorEvent event) { if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) { System.arraycopy(event.values, 0, gravity, 0, 3); } else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) { System.arraycopy(event.values, 0, geomagnetic, 0, 3); } // 仅当两个传感器数据都更新后才计算 if (gravity != null && geomagnetic != null) { boolean success = SensorManager.getRotationMatrix(rotationMatrix, null, gravity, geomagnetic); if (success) { SensorManager.getOrientation(rotationMatrix, orientation); // orientation[0] 是 azimuth(偏航角),范围 -π 到 π,需转为 0-360° float azimuth = (float) Math.toDegrees(orientation[0]); if (azimuth < 0) azimuth += 360; updateRadarPointer(azimuth); // 更新 UI 指针 } } } @Override public void onAccuracyChanged(Sensor sensor, int accuracy) {} };逻辑说明:
SensorManager.getRotationMatrix()根据重力与地磁向量构建旋转矩阵,getOrientation()从中提取欧拉角。orientation[0]即设备相对于正北的偏航角(azimuth),是雷达指针旋转的直接依据。参数azimuth为弧度制,需转为度数并归一化至[0, 360)区间。
3.3 地理围栏监听:动态计算妖怪坐标方位角并驱动 UI 更新
假设已从服务器获取附近妖怪坐标列表List<Monster>,每个Monster含lat,lng。需实时计算其相对于当前设备坐标的方位角(bearing):
private float calculateBearing(double currentLat, double currentLng, double targetLat, double targetLng) { double dLon = Math.toRadians(targetLng - currentLng); double lat1 = Math.toRadians(currentLat); double lat2 = Math.toRadians(targetLat); double y = Math.sin(dLon) * Math.cos(lat2); double x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon); double bearing = Math.toDegrees(Math.atan2(y, x)); return (bearing + 360) % 360; // 归一化到 [0, 360) } // 在 onLocationChanged() 中调用 @Override public void onLocationChanged(AMapLocation location) { if (location != null && location.getErrorCode() == 0) { double currentLat = location.getLatitude(); double currentLng = location.getLongitude(); for (Monster monster : monsterList) { float bearingToMonster = calculateBearing(currentLat, currentLng, monster.lat, monster.lng); // 将 bearingToMonster 与设备 azimuth 做差,得到雷达盘上相对角度 float relativeAngle = (bearingToMonster - currentAzimuth + 360) % 360; updateMonsterOnRadar(monster.id, relativeAngle); } } }calculateBearing()使用球面三角公式,比Location.distanceBetween()的bearingTo方法更精确,尤其在跨经度区域。relativeAngle即妖怪在雷达圆盘上的显示角度,驱动CompassView中Canvas.rotate()绘制图标。
4. 调试与优化:解决 Android 12+ 前台服务限制与传感器漂移
在 Android 12 及更高版本上,RadarService的存活率直接受ForegroundService行为变更影响;而传感器数据漂移则导致雷达指针“晃动”。这两类问题需针对性解决。
4.1 Android 12+ 前台服务保活:Notification Channel 与 Service Type 双重校验
Android 12 引入ForegroundServiceStartNotAllowedException,要求前台服务启动前必须满足:
- 已创建 Notification Channel;
startForeground()调用时传入的Notification必须关联该 Channel;AndroidManifest.xml中service的foregroundServiceType必须匹配。
// RadarService.java private void createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationChannel channel = new NotificationChannel( "radar_channel", "捉妖雷达服务", NotificationManager.IMPORTANCE_LOW); channel.setDescription("持续定位以显示附近妖怪"); NotificationManager manager = getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); } } @Override public void onCreate() { super.onCreate(); createNotificationChannel(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Notification notification = buildForegroundNotification(); startForeground(1, notification); // ID=1, Channel ID="radar_channel" return START_STICKY; } private Notification buildForegroundNotification() { Intent notificationIntent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); return new NotificationCompat.Builder(this, "radar_channel") .setContentTitle("捉妖雷达运行中") .setContentText("正在扫描附近妖怪...") .setSmallIcon(R.drawable.ic_radar) .setContentIntent(pendingIntent) .setOngoing(true) .build(); }验证方法:在 Android 12+ 设备上,进入「设置 > 应用 > 捉妖雷达 > 通知」,确认
radar_channel存在且未被用户关闭。若startForeground()报错,检查build.gradle中targetSdk是否 ≥31,且AndroidManifest.xml中service的foregroundServiceType是否与Notification的importance匹配(IMPORTANCE_LOW对应specialUse)。
4.2 传感器数据滤波:用低通滤波器抑制罗盘抖动
原始azimuth数据每秒更新 5~10 次,但存在高频抖动(±5°),导致雷达指针“抽搐”。加入一阶低通滤波器平滑:
private float filteredAzimuth = 0f; private static final float FILTER_ALPHA = 0.2f; // 滤波系数,0.1~0.3 之间 // 在 onSensorChanged() 中,计算完 azimuth 后: float rawAzimuth = (float) Math.toDegrees(orientation[0]); if (rawAzimuth < 0) rawAzimuth += 360; // 低通滤波:filtered = alpha * raw + (1-alpha) * filtered filteredAzimuth = FILTER_ALPHA * rawAzimuth + (1 - FILTER_ALPHA) * filteredAzimuth; updateRadarPointer(filteredAzimuth);FILTER_ALPHA控制响应速度与平滑度:值越小,越平滑但响应延迟越高;值越大,越灵敏但抖动残留越多。实测0.2在大多数设备上取得最佳平衡。
4.3 地图 SDK 渲染优化:避免MapView内存泄漏与离屏渲染异常
MapView是SurfaceView,其生命周期必须严格与Activity同步。常见泄漏点在于onDestroy()未调用mapView.onDestroy():
// MainActivity.java @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mapView = findViewById(R.id.mapView); mapView.onCreate(savedInstanceState); // 必须调用 } @Override protected void onResume() { super.onResume(); mapView.onResume(); } @Override protected void onPause() { super.onPause(); mapView.onPause(); } @Override protected void onDestroy() { super.onDestroy(); mapView.onDestroy(); // 关键!防止 Activity 销毁后 MapView 继续持有 Context }此外,在MapView上叠加自定义雷达 View 时,需禁用MapView的触摸事件透传,否则雷达 UI 无法响应点击:
<!-- activity_main.xml --> <com.amap.api.maps.MapView android:id="@+id/mapView" android:layout_width="match_parent" android:layout_height="match_parent" /> <!-- 雷达层,覆盖在 MapView 之上 --> <com.example.zhuoyao.radar.view.CompassView android:id="@+id/compassView" android:layout_width="200dp" android:layout_height="200dp" android:layout_centerInParent="true" android:clickable="true" android:focusable="true" />android:clickable="true"确保CompassView拦截触摸事件,避免穿透到下方MapView。
5. 进阶技巧:用 WorkManager 替代前台服务实现低功耗后台扫描
前台服务虽可靠,但持续唤醒 CPU 导致耗电高。对“非实时”捉妖场景(如每 5 分钟扫描一次),可用WorkManager替代,兼顾系统兼容性与电池寿命。
5.1 定义周期性定位 Worker
public class RadarWorker extends CoroutineWorker { public RadarWorker(@NonNull Context context, @NonNull WorkerParameters params) { super(context, params); } @NonNull @Override public Result doWork() { // 使用高德 SDK 获取一次定位 AMapLocationClient client = new AMapLocationClient(getApplicationContext()); AMapLocationClientOption option = new AMapLocationClientOption(); option.setOnceLocation(true); option.setNeedAddress(false); client.setLocationOption(option); CountDownLatch latch = new CountDownLatch(1); client.setLocationListener(location -> { if (location.getErrorCode() == 0) { // 上传坐标到服务器,查询附近妖怪 uploadAndFetchMonsters(location.getLatitude(), location.getLongitude()); } latch.countDown(); }); client.startLocation(); try { latch.await(10, TimeUnit.SECONDS); // 最大等待 10 秒 } catch (InterruptedException e) { return Result.failure(); } return Result.success(); } }5.2 注册周期性工作请求
// 在 Application.onCreate() 或首次启动时调用 private void scheduleRadarWork() { PeriodicWorkRequest radarWork = new PeriodicWorkRequestBuilder<RadarWorker>(15, TimeUnit.MINUTES) .setConstraints( new Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build()) .build(); WorkManager.getInstance(this) .enqueueUniquePeriodicWork( "radar_scan", ExistingPeriodicWorkPolicy.KEEP, radarWork); }PeriodicWorkRequest最小间隔为 15 分钟(Android 系统限制),适用于“后台静默扫描”场景。Constraints确保仅在网络可用、电量充足时执行,大幅降低耗电。此方案无法实现“实时雷达指针”,但可作为前台服务的节能降级方案,在用户锁屏后自动切换。
验证命令:在终端执行
adb shell dumpsys jobscheduler | grep com.example.zhuoyao.radar,可查看radar_scan工作是否已注册及下次执行时间。若未出现,检查WorkManager初始化是否在Application中完成,且AndroidManifest.xml中已声明android.permission.POST_NOTIFICATIONS(Android 12+)。
本文还有配套的精品资源,点击获取