ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android自助旅游系统开发:高德地图、GPS定位与轨迹回放实践

Android自助旅游系统开发:高德地图、GPS定位与轨迹回放实践 简介基于安卓平台的自助旅游系统毕业设计资源包面向安卓初学者与高校毕业设计学生完整实现旅游类应用的典型功能链路包括用户注册登录、景点信息查询、定位导航、地图模式切换、天气预报、交通路径规划以及周边住宿餐饮娱乐检索等。项目源码结构完整便于针对模块二次开发或撰写论文参考。压缩包共2000个文件约136.23MB以xml布局、java逻辑、png图片资源为主并包含json配置、so动态库、jar依赖及sql数据库脚本等覆盖界面搭建、网络请求、数据持久化等全流程要素适合导入开发环境直接运行调试。目前已有305人学习下载适合需要快速搭建自助旅游类项目、梳理定位与地图模块实现思路的开发者。通过研读各模块代码与资源配置可掌握安卓网络请求、地图服务接入、轨迹记录与本地存储等核心技能为独立完成毕业设计提供扎实参考。1. 自助旅游系统的核心痛点与功能导览自助游最考验人的并不是做攻略的那一刻而是到了陌生城市后手机地图、公共交通、住宿与餐饮决策都要在短时间内完成。基于Android手机平台的自助旅游系统解决的是这种“本地即时决策”问题打开App先获取自己的经纬度坐标再查景点在哪个位置步行或驾车过去要多久附近有哪些评价靠谱的餐馆和旅店。它是一个把LBS基于位置的服务常用能力串起来的完整Demo而不是商业级产品。系统涵盖四条主线用户注册与登录、景点查询含天气预报与路线规划、GPS定位与轨迹回放、周边POI检索。技术栈以Android原生 高德地图SDK SQLite为核心UI层采用Activity壳加Fragment页面结构。对毕业生来说这套代码能直接支撑需求分析、概要设计、详细设计三部分文档对在职开发者来说轨迹回放和区域进出提醒两处实现可以移植到巡检打卡、跑步轨迹等场景复用。2. 从架构到依赖Android项目模块划分与地图选型2.1 模块边界UI、Service、Provider 三层结构自助旅游系统的功能虽然看着不少但抽象后只有三件事人、地点、轨迹。所以工程结构按标准三层划分而不是让Activity里堆满逻辑。表现层负责页面渲染包含MainActivity、MapFragment、LoginActivity、RegisterActivity和TrackActivity它们只处理点击事件和页面刷新。业务层把用户校验、路径规划、POI检索、天气查询、轨迹采样封装成独立类对外暴露接口对内管理回调。数据层用SQLiteHelper管理用户表和轨迹表用SharedPreferences保存登录状态和地图模式偏好。这样的原因是地图SDK的异步回调很多如果页面直接持有地图实例屏幕旋转时很容易泄漏Activity。把业务对象独立出来MapView在onDestroy解绑业务层生命周期则跟随Application作用域。在Android Studio里新建工程时按照这三个层级建包之后每加一个功能都能对号入座不会出现一个Activity两千行的情况。2.2 高德地图SDK接入为什么选它而不是手动加载离线图定位与地图能力有两条路线一种是基于OpenStreetMap自建瓦片服务另一种是接入成熟地图SDK。最终选择高德因为它把地图、定位、POI搜索、路径规划、天气接口都放到同一套SDK和Key体系下不必自己拼接HTTP协议解析瓦片。百度地图也能做到类似效果但高德在步行路径规划的返回参数上更直观跨城路径的换乘描述更完整。这里只接入地图、定位、搜索三个库不引入导航SDK因为系统不需要实时语音引导只需要算路和在地图上画一条Polyline。这样安装包能小3MB左右冷启动时间也会缩短。2.3 Gradle依赖与权限声明模块级build.gradle中核心依赖如下。注意示例里用latest.release只是便于阅读实际项目务必锁定具体版本号否则SDK升级会带来不可控的类变更。dependencies { implementation com.amap.api:map2d:latest.release implementation com.amap.api:location:latest.release implementation com.amap.api:search:latest.release implementation com.android.support:support-v4:28.0.0 implementation com.squareup.okhttp3:okhttp:3.14.9 // 天气接口备用通道 }map2d体积小初始化快适合这种以POI列表和轨迹展示为主的界面。如果直接引入map3d布局中要用TextureMapView替换MapView初始化参数也略有差异。okhttp不是必须但高德天气服务的部分子Key在旧SDK上调用不稳定留一个HTTP客户端解析JSON天气会更可控。Manifest里的权限声明需要注意高德SDK要求定位、网络、Wi-Fi状态权限但Android 6.0以上运行时定位权限必须动态申请否则onLocationChanged永远拿不到GPS结果。SDK的Key存放在application标签的meta-data里覆盖安装时要确认com.amap.api.v2.apikey这个节点没有被混淆规则过滤。uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION/ uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION/ uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE/ uses-permission android:nameandroid.permission.CHANGE_WIFI_STATE/ uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE/ uses-permission android:nameandroid.permission.INTERNET/ application meta-data android:namecom.amap.api.v2.apikey android:value您的Key/ /application权限在6.0以上版本的动态申请思路进入地图页前检查checkSelfPermission为PackageManager.PERMISSION_DENIED时调用requestPermissions不能只停留在Manifest静态声明这步。另外如果项目用的是AndroidX记得把support-v4换成androidx对应依赖否则类冲突时高德初始化会抛出AMapCore.init异常。关于包结构下表列出的是系统最核心的类和功能归属模块涉及类责任说明地图展示MapFragment持有AMap实例管理Marker和Polyline定位LocationService封装AMapLocationClient回调定位结果路径规划RouteSearcher封装RouteSearch返回步行/驾车/公交路线周边检索PoiAroundSearcher按半径搜索餐饮、住宿、娱乐POI用户管理UserDao注册、登录、会话状态读写提示高德SDK各库的混淆规则要单独配置否则发布包里定位和地图都会失效。官方给的proguard规则保留com.amap.api.**和com.autonavi.**两个包名不要图省事全部keep会影响其他注解类。在proguard-rules.pro里至少加这三行-keep class com.amap.api.**{*;} -keep class com.autonavi.**{*;} -keep class com.loc.**{*;}混淆配置完成后建议先在debug包上验证一次地图加载和POI搜索再打release包。很多项目上线后发现地图白屏都是因为释放了核心库类导致的。3. 景点查询模块地图标注、路线规划与周边检索的落地实现3.1 地图初始化与经纬度标注景点查询的入口是地图页。在onActivityCreated中先拿到AMap实例再往地图上添加景点Marker。Marker的位置数据来自服务端返回的景点列表列表里包含景点名称、纬度、经度和简介。我们使用MarkerOptions对象时除了设定经纬度还应同时设置图标和锚点锚点默认在图片底部中心不调整的话会出现图标“浮”在坐标点上方的视觉误差。private void addSpotMarkers(AMap aMap, ListScenicSpot spotList) { for (ScenicSpot spot : spotList) { MarkerOptions options new MarkerOptions() .position(new LatLng(spot.getLatitude(), spot.getLongitude())) .title(spot.getName()) .snippet(spot.getShortDesc()) .icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_spot)) .anchor(0.5f, 1.0f); aMap.addMarker(options); } aMap.moveCamera(CameraUpdateFactory.newLatLngZoom(center, 12f)); }anchor(0.5f, 1.0f)把图标锚点定为横向居中、纵向底部这样坐标点真正指向景点门牌位置而非图标中心。moveCamera中的12f是城市级缩放级别景点分布较散的省界区域可以降到10f景区内可以升到15f。记住在加载景点列表前先做一次aMap.clear()否则切换城市后旧Marker还会残留在地图上导致同一个经纬度出现两个重叠图标。3.2 路径规划步行、驾车、公交三种模式点击景点列表项后系统需要计算从当前位置到景点的路线。高德RouteSearch是异步接口必须通过OnRouteSearchListener接收结果。模式不同构造查询参数的方式也不同步行用WalkRouteQuery驾车用DriveRouteQuery公交用BusRouteQuery。下面以步行规划为例这段代码也被用在景区内部景点切换场景RouteSearch routeSearch new RouteSearch(this); routeSearch.setRouteSearchListener(listener); RouteSearch.FromAndTo fromAndTo new RouteSearch.FromAndTo( new LatLng(startLat, startLng), new LatLng(endLat, endLng) ); RouteSearch.WalkRouteQuery walkQuery new RouteSearch.WalkRouteQuery( fromAndTo, RouteSearch.WALK_DEFAULT); routeSearch.calculateWalkRouteAsyn(walkQuery);WALK_DEFAULT表示高德默认的步行速度模型返回的步行时间会按4km/h估算。如果你想强制步行路线按最短距离、避开过街天桥可用RouteSearch.WALK_MULTI_PATH枚举。计算完成回调中从WalkRouteResult拿getPaths()再取getSteps()遍历得到PolylineOptions最后aMap.addPolyline()绘制。绘制路线时有一个性能细节getPolyline()返回的坐标点集合通常有几百个点直接传给PolylineOptions.addAll()没问题但如果你需要同时画多条备选线路公交的多个方案建议只保留第一条或点击时异步添加避免同一帧里创建多个巨大的List导致地图掉帧。3.3 周边餐饮住宿检索与POI搜索“周边”功能的本质是POI搜索以某个中心点为圆心按半径搜索指定类型的商家。高德PoiSearch需要先构造PoiSearch.Query传入关键字和城市再设置SearchBound传入圆点坐标和半径。这里最容易犯的错是SearchBound第三个布尔参数它表示传入的半径是否以米为单位。很多教程写new SearchBound(center, 2000, false)结果2000被当成公里搜索范围覆盖半个省。PoiSearch.Query query new PoiSearch.Query(餐饮, , 北京); query.setPageSize(20); query.setPageNum(0); PoiSearch poiSearch new PoiSearch(this, query); poiSearch.setBound(new PoiSearch.SearchBound(centerLatLng, 1500, true)); poiSearch.setOnPoiSearchListener(listener); poiSearch.searchPOIAsyn();setPageSize最大支持25这里设20主要为了几秒内拉完并展示一屏。返回的PoiResult.getPois()里每个PoiItem含有标题、地址和电话列表直接绑定title和snippet即可。如果想按“住宿”“娱乐”切换类别把query的关键字换成对应词就行。注意酒店类的POI名称往往很长用高德返回的getProvinceName()和getCityName()拼成副标题比直接显示getSnippet()更整齐。3.4 未来三天天气预报与地图模式切换天气预报如果不想额外引入SDK可以自己拉HTTP接口解析JSON。系统保留了两个入口优先用高德WeatherSearch如果Key权限受限就回退到HTTP方式。这里贴的是最简单的JSON解析逻辑用于把接口返回的forecasts数组映射成三个WeatherItem对象JSONObject result new JSONObject(response); JSONArray forecasts result.getJSONObject(result).getJSONArray(forecasts); ListWeatherItem list new ArrayList(); for (int i 0; i Math.min(forecasts.length(), 3); i) { JSONObject day forecasts.getJSONObject(i); WeatherItem item new WeatherItem( day.getString(date), day.getString(day_weather), day.getString(night_weather), day.getString(min_temp), day.getString(max_temp)); list.add(item); }这里字段名来自接口文档如果你的数据源字段叫low或high需要相应调整。判断未来三天逻辑是循环到forecasts.length()就停止取前三条即可。地图模式切换则简单很多三个RadioButton分别调用aMap.setMapType(AMap.MAP_TYPE_NORMAL)、AMap.MAP_TYPE_SATELLITE和AMap.MAP_TYPE_ROAD_MAP。ROAD_MAP在部分真机上表现和普通模式一样因为路况图层需要额外打开所以建议在切换代码后追加一行aMap.getUiSettings().setTrafficEnabled(true)否则用户会以为按钮失效。4. GPS定位与轨迹回放从定位权限到动画回放的完整链路4.1 定位参数设置与回调时机导航和轨迹记录共用一套定位服务但参数配置完全不同。轨迹记录必须高频刷新否则画出来的Path会变成折线。这里使用高德AMapLocationClient做原生定位初始化时通过AMapLocationClientOption控制回传频率和定位模式。AMapLocationClient locationClient new AMapLocationClient(mContext); AMapLocationClientOption option new AMapLocationClientOption(); option.setLocationMode(AMapLocationClientOption.AMapLocationMode.Hight_Accuracy); option.setInterval(2000); option.setOnceLocation(false); option.setMockEnable(false); option.setNeedAddress(false); locationClient.setLocationOption(option); locationClient.setLocationListener(location - { if (location.getErrorCode() 0) { onLocationChanged(location.getLatitude(), location.getLongitude(), location.getSpeed()); } });Hight_Accuracy模式会先尝试GPS再结合网络定位室外精度在5米左右。setInterval(2000)是每隔2秒回调一次这个间隔足够识别步行或骑行的轨迹拐弯。setNeedAddress(false)可以减少逆地理编码的耗时和流量因为我们只需要坐标不需要每次旋转街道名。把回调频率提高到1秒也没问题但存储压力会翻倍后文会提到用标志位过滤冗余点。4.2 区域进出提醒用Geofence触发音乐和通知系统要求在用户进入或离开设定区域时播放音乐提醒。高德的AMapGeofence封装了位置围栏的能力创建围栏时需要指定圆心坐标、半径、围栏id和触发回调类型。我们把围栏的中心点在景点地图页上通过长按事件来设置这样用户就能自定义“设置到达提醒”的位置。AMapGeofence fence new AMapGeofence.Builder() .setLatitude(targetLatitude) .setLongitude(targetLongitude) .setRadius(300) .setId(custom_remind) .setMissRound(0) .build(); geoFenceManager.addRoundGeoFence(fence); // 旧版本也可写为addGeofence(fence)setMissRound(0)表示连续漏掉多少次判定后放弃该次提醒设置成0是为了一针见血地触发。在GeofenceListener.onGeoFenceCreateFinished回调里判断geoFence.getAroundId()不为空再注册onEnter和onExit监听。播放音乐时要注意MediaPlayer的释放时机触发完马上reset()否则下次进入时会出现多个实例同时播放。区域提醒的边界精度会受定位频率影响定位间隔5秒时用户快速跑步可能会越过边界后才收到通知这是正常现象。4.3 轨迹采样用位移阈值过滤冗余点如果把每2秒一个的定位点全部写入SQLite爬一段山地后就是几千条数据回放时既不流畅也没必要。我们的采样策略很简单比较当前点与上一个已存点的直线距离超过2米才入库否则丢弃。private void saveTrackPoint(double lat, double lng, float speed) { if (lastPoint null) { insertTrackPoint(0, lat, lng, speed, System.currentTimeMillis()); lastPoint new LatLng(lat, lng); return; } float distance calculateDistance(lastPoint.latitude, lastPoint.longitude, lat, lng); if (distance 2.0f) { insertTrackPoint(trackId, lat, lng, speed, System.currentTimeMillis()); lastPoint new LatLng(lat, lng); } }calculateDistance可以用Location.distanceBetween原生方法比高德SDK接口更快且不依赖定位client。阈值取2米是因为一般步行者每秒移动1.5米左右2秒间隔产生的位移会大于2米所以不会丢失太多实际路径。如果要记录跑步轨迹可以把阈值改成5米减少急停转弯时的大量点。入库时把trackId关联到当前用户这样回放查询只取一条Active轨迹。4.4 动画回放从时间戳到地图上的动态折线回放模块读取SQLite中的轨迹点按时间升序排列然后通过动画逐步绘制Polyline。第一版直接用for循环一次性setPoints整个轨迹瞬间出现看不出前进趋势。改进后的方案是使用ValueAnimator在连续整数间插值每一次更新帧都截取轨迹列表的前N个点ValueAnimator animator ValueAnimator.ofInt(1, trackPoints.size()); animator.setDuration(800L * trackPoints.size()); animator.setInterpolator(new LinearInterpolator()); animator.addUpdateListener(animation - { int count (int) animation.getAnimatedValue(); polyline.setPoints(trackPoints.subList(0, count)); }); animator.start();800L * trackPoints.size()是总时长相当于每个点渲染0.8秒速度比较接近步行步频。如果轨迹点间距不均匀这种匀速动画会产生“前段后段速度不一致”的错觉更严谨的做法是用每个点的timestamp差值计算累计时间比例再把比例映射到ofInt的数值区间。回放结束后记得把polyline.setVisible(false)清理掉否则下一次回放会叠加在新路径上。回放页底部可以加一个SeekBar作为进度条拖动后调用animator.setCurrentPlayTime()把动画跳到对应轨迹索引这个交互比单纯动画回放更能体现“可回溯”的价值。5. 用户系统与数据持久化注册登录、偏好存储与SQLite轨迹库5.1 注册模块数据校验与密码哈希注册页是用户进入系统的第一道门。除了常规的非空和手机号位校验外密码存储方式比校验本身更重要。直接存明文会导致后续任何泄露都变成事故。这里对密码做SHA-256加盐哈希盐值用注册时间戳的十六进制字符串即使两个用户密码相同最终库里的hash也不同。public static User registerUser(String name, String password) { String salt Long.toHexString(System.currentTimeMillis()); String hash sha256(password, salt); User user new User(name, hash, salt); long id userDao.insert(user); user.setId(id); return user; }sha256方法内部使用MessageDigest实现传参盐和密码拼接后计算摘要。注意MessageDigest不是线程安全的不要把它设为static共享实例在多线程登录场景下会偶发重复摘要。注册成功后系统直接自动登录把isLogin写入SharedPreferences避免用户注册完还要再输一遍密码。注册页布局用TextInputLayout包裹两个EditText分别承载密码和确认密码错误提示直接绑定在控件上会更友好。5.2 登录会话与SharedPreferences登录页的校验逻辑是取输入的用户名到数据库查出password_hash和salt再用相同算法计算输入密码的哈希比对两个哈希是否一致。一致后创建会话保存用户基本信息到SharedPreferencesSharedPreferences.Editor editor getSharedPreferences(session, MODE_PRIVATE).edit(); editor.putBoolean(isLogin, true); editor.putInt(userId, user.getId()); editor.putString(userName, user.getName()); editor.putString(mapMode, normal); editor.apply();apply()是异步落盘比commit()更适合主线程调用。这里把mapMode也放进session是为了地图页在冷启动时能恢复用户上次选择的“卫星”或“路况”模式。退出登录时不只是finish()还要显式调用editor.clear().apply()并stop定位服务否则后台的AMapLocationClient会继续持有监听器被系统检测为耗电应用。5.3 数据库表设计用户表与轨迹表SQLiteOpenHelper的onCreate中执行建表语句。用户表和轨迹表分离通过user_id外键关联。轨迹表包含速度、定位精度和采样时间戳这些字段直接服务回放和以后扩展“平均配速”等统计。CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, salt TEXT NOT NULL, created_at TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE track ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, lat REAL NOT NULL, lng REAL NOT NULL, speed REAL DEFAULT 0, accuracy REAL DEFAULT 0, sample_time INTEGER NOT NULL );这里有几个设计点name加UNIQUE约束注册时捕获数据库层面的唯一冲突比先查一次再插入更可靠sample_time用INTEGER存Unix时间戳回放动画需要计算相邻点的时间差lat/lng用REAL类型存储而不是INTEGER高德返回的坐标是小数精度丢失会导致路线偏移。轨迹表要在user_id上建索引SQLite默认只在主键建索引没有索引时按用户查询轨迹会全表扫描。批量插入时可以用db.beginTransaction()包裹循环结束后setTransactionSuccessful()事务提交比逐条自动提交快5倍以上。5.4 异步修改数据库避免主线程卡顿定位回调每2秒产生一个点如果直接在回调里执行SQLite插入几十秒后主线程就开始掉帧。因为回调线程虽然是工作线程但为了后续地图更新仍需要切换到主线程切换本身没有访问主线程CPU数据库操作却会拖慢整体。常见的做法是用单线程Executor串行处理数据库写入private final ExecutorService dbExecutor Executors.newSingleThreadExecutor(); public void insertTrackAsync(TrackPoint point) { dbExecutor.execute(() - trackDao.insert(point)); }newSingleThreadExecutor保证所有写入按提交顺序排队避免并发插入导致SQLiteDatabaseLockedException。读取轨迹用于回放时建议单独用AsyncTask或LiveData不要在Executor里既写又读否则长事务会阻塞回放查询。这个设计对轨迹数据量较大的场景很重要否则App运行半个小时后插入速度会从每毫秒一次降到每帧一次。6. 调试与性能细节地图卡顿、定位漂移的排查技巧6.1 地图渲染卡顿的常见原因地图卡顿通常不是SDK的锅而是使用方式出了问题。最常见的是在onMapLoaded里一次性add几百个Marker而且每个Marker都用了独立的BitmapDescriptorFactory.fromBitmap()。正确做法是建立一个HashMap缓存icon Bitmap复用一个BitmapDescriptor。另一个容易被忽视的地方是Polyline的宽度如果设置width(80)且开启了setDottedLine(true)在某些GPU型号上会导致联动开销巨大宽度降到30f卡顿立消。6.2 定位漂移与多源融合定位漂移多半来自三种情况未加权限导致高德回退到网络定位、室内高楼反射GPS信号、以及用了模拟位置。排查时在定位回调里同时输出location.getProvider()和location.getAccuracy()。如果provider经常是network说明GPS权限没动态授权如果accuracy大于200米可以暂时丢弃该点而不是直接入库。处理漂移的技巧是用“加权滑动平均”保留最近三个坐标用0.2、0.3、0.5的权重做平方修正适用于步行场景。骑行或驾车场景不要用会带来明显的轨迹滞后。6.3 资源释放地图Page退出时的解绑顺序地图Activity退出时如果直接finishAMap会持有最后一个相机位置和Marker引用导致内存泄漏。正确顺序是先在onDestroy中aMap.clear()清除所有Marker和Polyline再调用mapView.onDestroy()最后将locationClient.stopLocation()。注意不要提前stopLocation否则退出动画会没有定位数据而白屏。6.4 用电量与定位频率的折中测试中发现连续2秒定位一小时耗电约4%游客观感尚可。如果是后台记录轨迹应把setInterval(5000)并使用AMapLocationClientOption.AMapLocationPurpose.Transport这个用途模式会自动降低卫星扫描次数。同时开启option.setKillProcess(false)防止用户在设置里手动停止服务时整个App崩溃这一项在小米和华为设备上特别常见需要适配。本文还有配套的精品资源点击获取
返回列表