山东大学创新实训项目个人博客——第三篇
本周是项目开始开发的第四周,主要完成了导航界面中高德API的集成工作。
在 build.gradle.kts中配置依赖:
//build.gradle.kts(Module:app) implementation("com.amap.api:3dmap-location-search:latest.integration")在manifest中配置相关权限、设置好API key、声明定位服务:
<!-- 网络权限 --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- 定位权限 --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <meta-data android:name="com.amap.api.v2.apikey" android:value="在高德控制台中创建的api-key" /> <!-- 在application标签下--> <service android:name="com.amap.api.location.APSService" />将高德官方下载的SDK内的so文件拷贝到与项目src目录同级的libs目录下,发现x86_64文件夹为空,说明高德官方已经不再支持在x86架构cpu上运行了,也就意味着Android Studio内置的基于x86架构的虚拟机无法运行高德SDK,也就意味着以后的开发不得不使用真机测试了:
复制完so文件后在build.gradle.kts中配置,确保原生库在不同CPU架构设备上正常运行:
defaultConfig { applicationId = "com.example.wenkang" minSdk = 29 targetSdk = 36 versionCode = 1 versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" ndk { abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64") } }在MainActivity的onCreate中配置如下参数,告诉高德SDK我们已经得到了用户的许可,才能开始使用高德SDK(这里硬编码为true,实际上应该询问用户后再设置为true):
// 地图SDK隐私合规 MapsInitializer.updatePrivacyShow(this, true, true) MapsInitializer.updatePrivacyAgree(this, true) // 定位SDK隐私合规 AMapLocationClient.updatePrivacyShow(this, true, true) AMapLocationClient.updatePrivacyAgree(this, true)将之前NavigationScreen中从ConsultationScreen复制过来的代码一并删除,整个界面为一个Column分为上中下三个部分,上部分占百分之五十,为地图部分,其余两部分占百分之二十五,分别是查询出来的医院列表(横向排布)和列表中选中的医院的详情。
这里主要介绍地图部分;初始化MapView:
val mapViewState = remember { MapView(context).apply { setMapView(this) } }由于MapView是传统的Android View,需要手动进行生命周期管理:
DisposableEffect(Unit) { mapViewState.onCreate(null) // 创建地图 mapViewState.onResume() // 恢复地图 onDispose { mapViewState.onPause() // 暂停地图 mapViewState.onDestroy() // 销毁地图 } }将其嵌入Compose中,其中mapView.map 获取核心的 AMap 对象,后续所有地图操作都基于它:
AndroidView( factory = { mapViewState }, modifier = Modifier.fillMaxSize() ) { mapView -> val aMap = mapView.map // 获取 AMap 对象 setAMap(aMap) // 设置地图点击事件 aMap.setOnMarkerClickListener { marker -> val hospital = markerHospitalMap[marker] hospital?.let { setSelectedHospital(it) } true } }以上是将高德地图MapView嵌入Compose的步骤,用户在同意了权限后,应该开启定位服务并将地图的定位中心切换到用户,搜索周围的医院。
在NavigationViewModel中写下获取用户定位方法,大致的流程就是检查权限后进行定位,如果定位不成功则使用默认定位,这里由于项目要求,定位成功后立刻调用了使用POI搜索医院的方法:
fun getCurrentLocation(context: Context? = null) { context?.let { // 先检查定位权限,权限验证不通过则使用默认位置 //使用try catch包裹,定位失败也使用默认位置 try { locationClient = AMapLocationClient(it) val option = AMapLocationClientOption().apply { locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy isOnceLocation = true isNeedAddress = true // 获取详细地址信息 httpTimeOut = 30000 // 设置超时时间30秒 isGpsFirst = true // GPS优先 } locationClient?.setLocationOption(option) locationClient?.setLocationListener(object : AMapLocationListener { override fun onLocationChanged(location: AMapLocation) { if (location.errorCode == 0) { val loc = Location("AMap").apply { latitude = location.latitude longitude = location.longitude accuracy = location.accuracy time = location.time } _currentLocation.value = loc // 定位成功后搜索附近医院 searchNearbyHospitals(context, location.latitude, location.longitude) } else { // 定位失败,使用默认位置 val defaultLocation = Location("Default").apply { latitude = 39.916345 longitude = 116.407174 } _currentLocation.value = defaultLocation searchNearbyHospitals(context, defaultLocation.latitude, defaultLocation.longitude) } } }) locationClient?.startLocation() } catch (e: Exception) { e.printStackTrace() // 发生异常时使用默认位置 val defaultLocation = Location("Default").apply { latitude = 39.916345 longitude = 116.407174 } _currentLocation.value = defaultLocation searchNearbyHospitals(context, defaultLocation.latitude, defaultLocation.longitude) } } }而在UI层,如果ViewModel成功获取到了用户定位,UI层监听到后就会进行更新:
LaunchedEffect(currentLocation) { currentLocation?.let { location -> val latLng = LatLng(location.latitude, location.longitude) // 移动相机到用户位置 aMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f)) // 添加蓝色标记 aMap?.addMarker( MarkerOptions() .position(latLng) .title("我的位置") .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_BLUE)) ) } }以上就是从用户打开界面、定位到用户位置到地图重定位并使用蓝色进行标记的全过程。POI搜索功能的实现大致相同,同样是定位到用户位置后,根据用户的位置去查询附近的医院并返回到hospitals中,UI层监听到hospitals发生改变后即在地图上使用红色进行标记,并将hospitals列表展示在下方的横向列表中,列表的selected属性发生变化时切换地图的聚焦中点,也就实现了追踪不同医院的效果:
// 搜索附近医院(使用高德 POI 搜索) fun searchNearbyHospitals(context: Context? = null, latitude: Double = 39.916345, longitude: Double = 116.407174) { if (context == null) return try { // 查询条件:关键词"医院",搜索类型为综合医院 val query = PoiSearch.Query("三甲医院", "", "") // 设置搜索范围 val searchBound = PoiSearch.SearchBound( LatLonPoint(latitude, longitude), 100000 // 半径 ) query.pageSize = 20 // 创建搜索实例 val poiSearch = PoiSearch(context, query) poiSearch.bound = searchBound poiSearch.setOnPoiSearchListener(object : PoiSearch.OnPoiSearchListener { override fun onPoiSearched(result: PoiResult?, code: Int) { if (code == 1000 && result != null && result.pois != null) { val hospitals = result.pois.map { poi -> HospitalWithLocation( name = poi.title ?: "未知医院", address = poi.snippet ?: poi.adName ?: "地址未知", department = "综合医院", level = "三级甲等", rating = 4.5f, // 默认评分 latitude = poi.latLonPoint?.latitude ?: latitude, longitude = poi.latLonPoint?.longitude ?: longitude ) } _hospitals.value = hospitals } else { // 搜索失败时使用预设模拟数据作为保底 loadMockHospitals() } } override fun onPoiItemSearched(item: PoiItem?, code: Int) {} }) poiSearch.searchPOIAsyn() } catch (e: Exception) { e.printStackTrace() loadMockHospitals() } }// 监听医院列表变化,更新地图标记 LaunchedEffect(hospitals) { // 清除旧标记 markers.forEach { it.remove() } setMarkers(emptyList()) setMarkerHospitalMap(emptyMap()) // 添加新标记 val newMarkers = mutableListOf<Marker>() val newMarkerHospitalMap = mutableMapOf<Marker, HospitalWithLocation>() hospitals.forEach { val latLng = LatLng(it.latitude, it.longitude) val marker = aMap?.addMarker( MarkerOptions() .position(latLng) .title(it.name) .snippet(it.address) .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED)) ) marker?.let {m -> newMarkers.add(m) newMarkerHospitalMap[m] = it } } setMarkers(newMarkers) setMarkerHospitalMap(newMarkerHospitalMap) }LazyRow( modifier = Modifier.fillMaxWidth(), contentPadding = PaddingValues(horizontal = 16.dp) ) { items(hospitals) { HospitalCard( hospital = it, isSelected = selectedHospital?.name == it.name, onSelect = { setSelectedHospital(it) // 点击医院时,地图移动到该医院位置 val latLng = LatLng(it.latitude, it.longitude) aMap?.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15f)) } ) } }以上就是本项目集成高德SDK的过程,用于定位用户并给用户推荐附近的三甲医院。不过后端的同学跟我说医院的推荐将由他们来完成,他们会提供一个根据数据库推荐四家附近医院并返回医院更加详细信息的方法,不需要调用高德POI搜索办法了,后面我也会根据具体的后端实现进行修改。目前实现的效果如下:
接下来的工作是完成注册界面(拖了两周了),优化主界面的布局(搬到真机上运行发现界面的排布还是有些问题),以及完成导航阶段的前一个阶段——问诊阶段,这部分后端的api已经实现,我可以进行更具体的适配了,比如实现流式输出等等。
