Android Studio降雨热力图

image-20230608163650808

基础配置

AndroidManifest.xml

配置定位服务和百度地图key

1
2
3
4
5
6
7
8
9
<!-- 百度地图定位服务-->
<service android:name="com.baidu.location.f"
android:enabled="true"
android:process=":remote">
</service>
<!--百度地图配置key-->
<meta-data
android:name="com.baidu.lbsapi.API_KEY"
android:value="填写百度地图key" />

加权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    <!-- 访问网络,进行地图相关业务数据请求,包括地图数据,路线规划,POI检索等 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 获取网络状态,根据网络状态切换进行数据请求网络转换 -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- 读取外置存储。如果开发者使用了so动态加载功能并且把so文件放在了外置存储区域,则需要申请该权限,否则不需要 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!-- 写外置存储。如果开发者使用了离线地图,并且数据写在外置存储区域,则需要申请该权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

<!-- 这个权限用于进行网络定位-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission>
<!-- 这个权限用于访问系统接口提供的卫星定位信息-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>
<!-- 用于访问wifi网络信息,wifi信息会用于进行网络定位-->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>

<!-- 这个权限用于获取wifi的获取权限,wifi信息会用来进行网络定位-->
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"></uses-permission>
<!--读取手机状态-->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

下载sdk

放到jinLibs下

image-20230608162854413

build.gradle

添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
android {
...
defaultConfig {
ndk {
// 设置支持的SO库架构(开发者可以根据需要,选择一个或多个平台的so)
abiFilters "armeabi", "armeabi-v7a", "arm64-v8a", "x86","x86_64"
}
}
...
//百度地图api用
sourceSets {
main {
jniLibs.srcDir 'libs'
}
}

}
dependencies {
...
implementation 'com.squareup.okhttp3:okhttp:4.8.1' //http请求
implementation 'com.google.code.gson:gson:2.8.8' //GSON解析JSON
implementation files('libs\\BaiduLBS_Android.jar') //百度地图sdk
}//导入了sdk就不用导入其他的百度地图以来了了,sdk基本都包含了,否则会发生冲突

生成地图

初始化

调整缩放

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//地图初始化
private fun initloc() {
//定位初始化
mLocationClient = LocationClient(requireActivity())
//通过LocationClientOption设置LocationClient相关参数
val option = LocationClientOption()
option.isOpenGps = true // 打开gps
option.setCoorType("bd09ll") // 设置坐标类型
option.setScanSpan(1000)
option.setAddrType("all")
option.setIsNeedAddress(true) // 可选,设置是否需要地址信息,默认不需要
option.setIsNeedLocationDescribe(true) // 可选,设置是否需要地址描述
//设置locationClientOption
mLocationClient!!.locOption = option
//注册LocationListener监听器
val myLocationListener = MyLocationListener()
mLocationClient!!.registerLocationListener(myLocationListener)
//设置缩放
//缩放级别
val builder = MapStatus.Builder()
builder.zoom(8.0f)
mBaiduMap?.setMapStatus(MapStatusUpdateFactory.newMapStatus(builder.build()))
//开启地图定位图层
mLocationClient!!.start()
//设置当前视图位置
mBaiduMap?.animateMapStatus(preStatus) //动画的方式到中间
}

//定位监听器
inner class MyLocationListener : BDAbstractLocationListener() {
override fun onReceiveLocation(location: BDLocation) {
//mapView 销毁后不在处理新接收的位置
if (location == null || mMapView == null) {
return
}
val locData = MyLocationData.Builder()
.accuracy(location.radius) // 此处设置开发者获取到的方向信息,顺时针0-360
.direction(location.direction).latitude(location.latitude)
.longitude(location.longitude).build()
mBaiduMap?.setMyLocationData(locData)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//地图初始化
private fun initloc() {
//定位初始化
mLocationClient = LocationClient(requireActivity())
//通过LocationClientOption设置LocationClient相关参数
val option = LocationClientOption()
option.isOpenGps = true // 打开gps
option.setCoorType("bd09ll") // 设置坐标类型
option.setScanSpan(1000)
option.setAddrType("all")
option.setIsNeedAddress(true) // 可选,设置是否需要地址信息,默认不需要
option.setIsNeedLocationDescribe(true) // 可选,设置是否需要地址描述
//设置locationClientOption
mLocationClient!!.locOption = option
//注册LocationListener监听器
val myLocationListener = MyLocationListener()
mLocationClient!!.registerLocationListener(myLocationListener)
//设置缩放
//缩放级别
val builder = MapStatus.Builder()
builder.zoom(8.0f)
mBaiduMap?.setMapStatus(MapStatusUpdateFactory.newMapStatus(builder.build()))
//开启地图定位图层
mLocationClient!!.start()
//设置当前视图位置
mBaiduMap?.animateMapStatus(preStatus) //动画的方式到中间
}

//定位监听器
inner class MyLocationListener : BDAbstractLocationListener() {
override fun onReceiveLocation(location: BDLocation) {
//mapView 销毁后不在处理新接收的位置
if (location == null || mMapView == null) {
return
}
val locData = MyLocationData.Builder()
.accuracy(location.radius) // 此处设置开发者获取到的方向信息,顺时针0-360
.direction(location.direction).latitude(location.latitude)
.longitude(location.longitude).build()
mBaiduMap?.setMyLocationData(locData)
}
}
1

显示定位

监听器,获取当前位置,根据定位数据设置状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
inner class MyLocationListener : BDAbstractLocationListener() {
override fun onReceiveLocation(location: BDLocation) {
...
//获取经纬度 并保留两位小数''
preLatitude = String.format("%.2f", location.latitude).toDouble()
preLongitude = String.format("%.2f", location.longitude).toDouble()

//获取当前地址
preAddress = location.addrStr

//保证经纬度
val ll = LatLng(location.latitude, location.longitude)
//设置位置状态
preStatus = MapStatusUpdateFactory.newLatLng(ll)
}
}

动画方式回到定位位置

1
2
3
4
5
6
//点击按钮回到当前位置
val setStatusBtn = binding.setStatusBtn
setStatusBtn.setOnClickListener {
//设置当前视图位置
mBaiduMap?.animateMapStatus(preStatus) //动画的方式到中间
}

获取降雨数据

组建城市列表

预设几个城市,生成地图步骤获取当前位置的经纬度数据

1
2
3
4
5
6
7
8
9
10
11
12
private fun getRainData() {
thread {
//城市列表
val cities = listOf(
Pair(preLatitude, preLongitude),//当前位置
Pair(39.90, 116.41), // 北京
Pair(31.23, 121.47), // 上海
...
) // 假设要查询的城市经纬度列表

}
}

准备类和取数据方法

查看和风天气实例数据结构和属性,构建类

观察逐小时天气预报返回数据,我们需要的是hourly里的pop数据,并希望把24小时的数据组成一个列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"code": "200",
"updateTime": "2021-02-16T13:35+08:00",
"fxLink": "http://hfx.link/2ax1",
"hourly": [
{
"fxTime": "2021-02-16T15:00+08:00",
"temp": "2",
"icon": "100",
"text": "晴",
"wind360": "335",
"windDir": "西北风",
"windScale": "3-4",
"windSpeed": "20",
"humidity": "11",
"pop": "0",
"precip": "0.0",
"pressure": "1025",
"cloud": "0",
"dew": "-25"
},
...

因此我们需要先准备好类来接收数据,并准备读取pop列表的方法。并设置方法,提取需要的数据属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//接收api逐小时数据hourly类
data class HourlyData(
val fxTime: String,
val temp: String,
val icon: String,
val text: String,
val wind360: String,
val windDir: String,
val windScale: String,
val windSpeed: String,
val humidity: String,
val pop: String,
val precip: String,
val pressure: String,
val cloud: String,
val dew: String
)

//获取降雨概率方法
class CityRainProbability(private val hourlyData: List<HourlyData>) {
//降雨概率
fun getHourlyPop(): List<Double> {
return hourlyData.map { it.pop.toDouble() }
}
}

调用API组成降雨数据

遍历列表,解析读取JSON

getRainData()中补充,遍历城市列表,请求解析完数据后,构造成city类,并存入城市降雨列表citiesRain

1
2
3
4
5
6
7
8
//获取每个城市降雨数据,先放入citiesRain中
for (city in cities) {
val location = city.second.toString() + "," + city.first.toString()
println(location)
val url = "https://devapi.qweather.com/v7/weather/24h?location=$location&key=$key"
println(url)
readJSONData(url, city)
}

city类,包含了经纬度信息和24小时降雨概率列表

1
2
3
4
5
//城市名称,降雨概率
data class City(
val latLng: LatLng,
val hourlyProbabilities: List<Double>?
)

实例化类,获取降雨数据

主要分为两步,首先请求数据 ,然后解析数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//请求数据
private fun readJSONData(url: String, city: Pair<Double, Double>) {
thread {
try {
val client = OkHttpClient()
val request = Request.Builder().url(url).build()
val response = client.newCall(request).execute()
val responseData = response.body?.string()
if (responseData != null) {
dealRainData(responseData, city)
Log.d("responseData", responseData)
// println(responseData)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}

//提取降雨数据
private fun dealRainData(JsData: String, city: Pair<Double, Double>) {
val gson = Gson()
val cityData = gson.fromJson(JsData, JsonObject::class.java)
val hourlyData =
gson.fromJson(cityData.getAsJsonArray("hourly"), Array<HourlyData>::class.java).toList()
val City = CityRainProbability(hourlyData)
hourlyPop = City.getHourlyPop()// 获取到一个城市24小时天气数据
val hourlyRainProb = hourlyPop?.toList()
val thiscity = City(LatLng(city.first, city.second), hourlyRainProb) //单个城市降雨数据
citiesRain.add(thiscity) //插入城市降雨数据列表
}

组成降雨数据citiesRain,是一个City类的列表

1
private val citiesRain = mutableListOf<City>()//城市降雨数据

显示热力图和信息窗

热力图

帧数据

构建24帧数据

提取城市列表每个城市经纬度和当前小时的降雨概率组成代表当前小时的帧数据,降雨概率作为权重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private fun showheat() {
// 创建热力图数据
val builder = HeatMap.Builder()
// 添加热力图数据点
//构造24帧数据
val frames = MutableList(24) { mutableListOf<WeightedLatLng>() }
// 遍历每个城市
citiesRain.forEach { city ->
val hourProbabilities = city.hourlyProbabilities
// 遍历每个小时的降雨概率
hourProbabilities?.forEachIndexed { index, probability ->
val weightedLatLng = WeightedLatLng(city.latLng, probability)
frames[index].add(weightedLatLng)
}
}
builder.weightedDatas(frames)

}

配置热力图

设置帧变化动画属性,热力图半径,渐变颜色,透明度

热力图权值范围0-100

添加热力图覆盖物

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private fun showheat(){
// 设置开始动画属性:开启初始动画,时长100毫秒,动画缓动函数类型为线性
val init = HeatMapAnimation(true, 100, HeatMapAnimation.AnimationType.Linear)
// 设置帧动画属性:开启帧动画,时长10000毫秒,动画缓动函数类型为线性
val frame = HeatMapAnimation(true, 10000, HeatMapAnimation.AnimationType.Linear)
builder.initAnimation(init)
builder.frameAnimation(frame)
// 设置热力图半径范围
builder.radius(35)
// 设置热力图渐变颜色
val colors = intArrayOf(
Color.rgb(255, 0, 0), Color.rgb(0, 225, 0), Color.rgb(0, 0, 200)
)
builder.gradient(Gradient(colors, floatArrayOf(0.2f, 0.5f, 1.0f)))
builder.maxIntensity(100.0f)
builder.opacity(0.8)
val heatMapData = builder.build()
Log.d("showHeat", "添加覆盖物")
// 添加热力图覆盖物
mBaiduMap?.addHeatMap(heatMapData)
mBaiduMap?.startHeatMapFrameAnimation()
}

进度条

进度条

1
2
3
4
5
6
7
<ProgressBar
android:id="@+id/determinateBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="24"
android:progress="1"/>

回调动态热力图帧索引,给进度条赋值,让进度条提示当前是第几帧(小时)的热力图

1
2
3
4
5
6
7
val progressText = binding.progressText //进度条提示文字
// 回调动态热力图帧索引
mBaiduMap?.setOnHeatMapDrawFrameCallBack { indexCallBack -> // 更新进度条和帧数
progressBar.progress = indexCallBack
// Log.d("帧数",indexCallBack.toString())
progressText.text = "$indexCallBack 小时后"
}

信息窗

遍历降雨数据找最大值,记录索引和最值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//计算当前位置最大降雨概率
private fun countRainpro() {
val preCity = citiesRain.firstOrNull()
maxRainIndex = -1
maxRainValue = Double.MIN_VALUE

preCity?.let { city ->
val attributeList = city.hourlyProbabilities

if (attributeList != null) {
for ((index, value) in attributeList.withIndex()) {
if (value > maxRainValue) {
maxRainValue = value
maxRainIndex = index
}
}
}
}
}

设置信息窗内容和位置

1
2
3
4
5
6
7
8
9
10
11
12
if (maxRainValue == 0.0) {
//24小时内不会下雨
messageBtn.text = "$preAddress \n24小时内不会下雨"//信息窗显示当前地址

} else {
var rat = maxRainValue.toInt()
messageBtn.text = "$preAddress \n $maxRainIndex 小时后有$rat %概率会下雨"
}
//构造InfoWindow
//point 描述的位置点
//-100 InfoWindow相对于point在y轴的偏移量
mInfoWindow = InfoWindow(messageBtn, LatLng(preLatitude, preLongitude), -100)

监听器中使InfoWindow生效,这样子就能满足当移动地图时,信息窗能够保持在初始位置,不会乱飞。

1
2
3
4
5
6
7
8
9
inner class MyLocationListener : BDAbstractLocationListener() {
override fun onReceiveLocation(location: BDLocation) {
...
//实时更新信息窗位置
if (mInfoWindow != null) {
mBaiduMap?.showInfoWindow(mInfoWindow)
}
}
}

遇到的问题

无法在模拟器显示

进行真机调试,数据线连接安卓手机,手机开启usb调试,修改编辑器运行设备为手机

异步问题

协程方法/用按钮