最终实现效果:
WeatherTest
代码目录结构
app功能拆解
关键字定位搜索
这里通过引入高德API 或者高德SDK的接口去实现,可以去高德开发者平台进行注册申请。
通过网络请求(url:https://restapi.amap.com/v3/assistant/inputtips?key=&keywords=)数据,这样就能返回对应data形如:
*PlaceResponse(tips=[Place(id=B0JBTOADDY, name=商丘·D.C服饰, district=河南省商丘市夏邑县, adcode=411426, location=116.138028,34.236249, address=县府西路与人和街中段交叉口东40米, typecode=061100, city=[]), Place(id=BV10266369, name=大草坊首末站(公交站), district=广东省东莞市, adcode=441900, location=113.798080,23.048478, address=35路, typecode=150700, city=[]), Place(id=B0JBH705HE, name=大厂房烧烤总店, district=山东省淄博市张店区, adcode=370303, location=118.015559,36.866099, address=世纪路与裕民路交叉口东南120米, typecode=050101, city=[]), Place(id=B0J277H5VB, name=德长防水补漏公司, district=广东省东莞市, adcode=441900, location=113.784463,23.026726, address=东城中路23-25号, typecode=070000, city=[]), Place(id=B0J2F73W9Q, name=东创防水补漏店, district=广东省东莞市, adcode=441900, location=113.759255,23.008477, address=东城街道嘉宏振兴中心606, typecode=071200, city=[]), Place(id=B0JD3MC057, name=大厨房烧烤涮三店, district=河北省唐山市路北区, adcode=130203, location=118.143903,39.665534, address=友谊东辅路与长宁西道交叉口, typecode=050100, city=[]), Place(id=B0JUJMMKB6, name=稻草坊手擀面(翡翠100店), district=河南省驻马店市驿城区, adcode=411702, location=114.016413,33.006228, address=文明路与通达路东100米路南, typecode=050100, city=[]), Place(id=B0JUK7WD03, name=德诚服饰, district=浙江省杭州市临平区, adcode=330113, location=120.330442,30.373481, address=钱塘社区五科村滨岸上44号, typecode=170300, city=[]), Place(id=B0JAZMWU5A, name=多彩服饰, district=河南省平顶山市叶县, adcode=410422, location=113.456958,33.533393, address=004县道与330省道交叉口西北120米, typecode=061100, city=[]), Place(id=B0JD47RGK4, name=大成防水, district=广东省广州市白云区, adcode=440111, location=113.230580,23.145573, address=高桥路58号A02铺, typecode=060603, city=[])], status=1, info=OK, infocode=10000, count=10)*
talk is cheap ,just see the fucking code
searchPlaceEdit.addTextChangedListener { editable ->
recyclerView.visibility = View.GONE
bgImageView.visibility = View.VISIBLE
viewModel.placeList.clear()
viewModel.currentPlaceList.clear()
adapter.notifyDataSetChanged()
val content = editable.toString()
if (content.isNotEmpty()){
viewModel.searchPlaces(content)
}
}
............
val placeLiveData = Transformations.switchMap(searchLiveData){ query ->
Repository.searchPlaces(query)
}
fun searchPlaces(query: String){
searchLiveData.value = query
}
...........
fun searchPlaces(query: String) = liveData(Dispatchers.IO) {
val result = try {
val placeResponse = WeatherNetwork.searchPlaces(query)
if (placeResponse.count != "0"){
val places = placeResponse.tips
Result.success(places)
}else{
Result.failure(RuntimeException("response counts is ${placeResponse.count}"))
}
}catch (e: Exception){
Result.failure<List<Place>>(e)
}
emit(result)
}
..........
suspend fun searchPlaces(query: String) = placeService.searchPlaces(query).await()
..........
private suspend fun <T> Call<T>.await(): T {
return suspendCoroutine { continuation ->
//异步发送网络请求
enqueue(object : Callback<T> {
//请求成功时回调
override fun onResponse(call: Call<T>, response: Response<T>) {
val body = response.body()
Log.d("gaorui", "WeatherNetwork - body = " + body)
if(body != null){
continuation.resume(body)
}else{
continuation.resumeWithException(RuntimeException("response body is null"))
}
}
//请求失败时回调
override fun onFailure(call: Call<T>, t: Throwable) {
Log.d("gaorui", "WeatherNetwork - onFailure ")
continuation.resumeWithException(t)
}
})
}
}
.............
@GET("v3/assistant/inputtips?key=${WeatherApplication.KEY}")
fun searchPlaces(@Query("keywords") keywords: String): Call<PlaceResponse>
..............
viewModel.placeLiveData.observe(this, Observer { result ->
val places = result.getOrNull()
if (places != null){
recyclerView.visibility = View.VISIBLE
bgImageView.visibility = View.GONE
viewModel.placeList.clear()
viewModel.currentPlaceList.clear()
viewModel.placeList.addAll(places)
adapter.notifyDataSetChanged()
}else{
recyclerView.visibility = View.GONE
bgImageView.visibility = View.VISIBLE
viewModel.placeList.clear()
viewModel.currentPlaceList.clear()
adapter.notifyDataSetChanged()
Toast.makeText(activity, "未能查询到任何地点", Toast.LENGTH_SHORT).show()
result.exceptionOrNull()?.printStackTrace()
}
})
..............
data class PlaceResponse(val tips: ArrayList<Place>, val status: String, val info: String, val infocode: String, var count: String)
data class Place(val id: Any, var name: String, var district: String, val adcode: String,
val location: Any, val address: Any, val typecode: String, val city: ArrayList<String>)
这里参考了guolin大佬的网络数据请求设计流程,此种设计可以说是面向切面的一种方式。期间实现此功能的时候,有遇到过返回的数据为空的情况,没有url请求报错。这个时候需要查看数据类的类型是否和本次查询到的json数据对应上了,有的时候某个字段会返回null,造成字段匹配错误,数据为空的情况。到这里此功能基本上是跑通了,剩下的就是代码优化,本次不做考虑。
精准定位
这里使用高德SDK的接口,进行精准定位,因为使用api的话,本地发现在局域网的时候竟然返回的数据为空(本身json数据为空,区别于上方提到过的情况),但是使用SDK 进行查询position的时候就不会出现链接局域网为空 的情况,不太清楚SDK 内做了啥特殊处理,没有深究,有知道的大佬可以指导下ha。
对于SDK 的使用,官网也有指导文档,这里就不用多说了,show the fucking code。
placeFragment_current_place.setOnClickListener { floatingButton ->
// viewModel.setCurrentIP(Util.getIpAddress(mContext))
// viewModel.sendRequestWithOKHttp()
val option: AMapLocationClientOption = AMapLocationClientOption()
//声明定位回调监听器
val mLocationListener = AMapLocationListener {
if (it != null) {
if (it.getErrorCode() == 0) {
//可在其中解析amapLocation获取相应内容。
Log.e("gaorui", "AmapSuccess - placeFragment - AMapLocationListener - lat = ${it.latitude} , lng = ${it.longitude}, Street = ${it.poiName}")
recyclerView.visibility = View.VISIBLE
bgImageView.visibility = View.GONE
viewModel.placeList.clear()
viewModel.currentPlaceList.clear()
progress_main_fragment.visibility = View.GONE
adapter.notifyDataSetChanged()
viewModel.searchPlaces(it.poiName)
}else {
//定位失败时,可通过ErrCode(错误码)信息来确定失败的原因,errInfo是错误信息,详见错误码表。
Log.e("gaorui","location Error, ErrCode:"
+ it.getErrorCode() + ", errInfo:"
+ it.getErrorInfo())
mLocationClient?.stopLocation();
progress_main_fragment.visibility = View.GONE
Toast.makeText(context, "get location error!", Toast.LENGTH_SHORT).show()
}
}
}
AMapLocationClient.updatePrivacyShow(context,true,true)
AMapLocationClient.updatePrivacyAgree(context,true)
//初始化定位
mLocationClient = AMapLocationClient(context)
//设置定位回调监听
mLocationClient?.setLocationListener(mLocationListener)
option.setOnceLocation(true)
mLocationClient?.setLocationOption(option)
progress_main_fragment.visibility = View.VISIBLE
mLocationClient?.startLocation()
Log.e("gaorui", "startLocation ")
}
到这里关于定位的功能基本就搞好了,接下来就是天气。
天气查询
这里使用免费的和风天气API进行请求数据,有更好的选择也可以替换掉。当上面两个定位功能做好之后,接下来需要把获取到的location位置、location name传递给显示temperature的界面。
show fucking code:
recyclerView.setOnItemClickListener(object : RecyclerViewExt.OnItemClickListener{
override fun onItemClick(
parent: RecyclerView.Adapter<*>?,
vh: RecyclerView.ViewHolder,
position: Int
) {
Log.d("gaorui", "setOnItemClickListener - position = " + vh.position +
", transitionName = " + (vh.itemView.findViewById(R.id.placeName) as View).transitionName )
val bundle: Bundle? = this@PlaceFragment.activity?.let {
ActivityOptionsCompat.makeSceneTransitionAnimation(
it,
Pair(vh.itemView.findViewById(R.id.placeName) as View, (vh.itemView.findViewById(R.id.placeName) as View).transitionName),
Pair(placeFragment_current_place, resources.getString(R.string.share_place_name))
).toBundle()
}
val intent = Intent(context, TempActivity::class.java).apply {
if (viewModel.currentPlaceList.isEmpty()) {
putExtra(TempActivity.PLCAE_NAME, viewModel.placeList[position].name)
putExtra(TempActivity.PLCAE_POSITION, (vh.itemView.findViewById(R.id.placeName) as View).transitionName)
putExtra(TempActivity.PLCAE_LOCATION, viewModel.placeList[position].location.toString())
} else {
putExtra(TempActivity.PLCAE_NAME, viewModel.currentPlaceList[position].name)
putExtra(TempActivity.PLCAE_POSITION, (vh.itemView.findViewById(R.id.placeName) as View).transitionName)
putExtra(TempActivity.PLCAE_LOCATION, viewModel.currentPlaceList[position].location)
}
}
startActivity(intent, bundle)
// fragmentToActivity?.transformToActivity(viewModel.placeList[position].location.toString(), viewModel.placeList[position].name)
}
override fun onItemLongClick(vh: RecyclerView.ViewHolder?, position: Int) {
Toast.makeText(context, " onItemLongClick", Toast.LENGTH_SHORT).show()
}
})
这里使用transition动画,添加了一点点的效果。废话不多说,接下来查询天气。
查询天气用的和风SDK,具体申请详见官网。这里把上一步传递过来的locaiton传递给SDK,然后剩下就是展示数据,写的有点粗鄙,凑合看,当然也可以使用dispatcher优雅下,show the fucking code:
fun searchTempOrRefresh(location:String) {
thread {
viewModel.searchPlaceTempUsingSDK(query = location, listener = object : QWeather.OnResultWeatherNowListener {
override fun onError(e: Throwable) {
Log.e("gaorui", "getWeather onError: " + e)
temp_swipe.isRefreshing = false
}
override fun onSuccess(weatherBean : WeatherNowBean) {
Log.e("gaorui", "getWeather onSuccess: " + Gson().toJson(weatherBean))
//先判断返回的status是否正确,当status正确时获取数据,若status不正确,可查看status对应的Code值找到原因
if (Code.OK == weatherBean.getCode()) {
val now :WeatherNowBean.NowBaseBean = weatherBean.getNow()
toDayDate = now.obsTime.split("T")[0]
runOnUiThread {
temp_placetemp.text = now.temp
temp_placetemp_feel.text = now.feelsLike
temp_placetemp_weatherkind.text = now.text
temp_placetemp_windscale.text = now.windScale
temp_placetemp_humidity.text = now.humidity
}
} else {
//在此查看返回数据失败的原因
val code = weatherBean.getCode();
Log.e("gaorui", "failed code: " + code);
Toast.makeText(this@TempActivity, "未能查询到任何地点", Toast.LENGTH_SHORT).show()
}
temp_swipe.isRefreshing = false
}
}, isFore = false)
}
}
天气预测
与上方天气查询大同小异,这里就不多说了,show the fucking code:
fun searchTempOrRefresh(location:String) {
thread{
Thread.sleep(1000)
viewModel.searchPlaceTempUsingSDK(query = location, isFore = true, listener = object : QWeather.OnResultWeatherDailyListener {
override fun onError(e: Throwable) {
Log.e("gaorui", "3d - getWeather onError: " + e)
temp_swipe.isRefreshing = false
}
override fun onSuccess(p0: WeatherDailyBean?) {
//先判断返回的status是否正确,当status正确时获取数据,若status不正确,可查看status对应的Code值找到原因
if (Code.OK == p0?.getCode()) {
val dailyList :List<WeatherDailyBean.DailyBean> = p0.daily
for (daily : WeatherDailyBean.DailyBean in dailyList) {
Log.e("gaorui", "3d - getWeather onSuccess: " + Gson().toJson(daily))
}
tempInfoArrayList.clear()
tempInfoArrayList.addAll(dailyList)
runOnUiThread {
adapter.notifyDataSetChanged()
}
} else {
//在此查看返回数据失败的原因
val code = p0?.getCode();
Log.e("gaorui", "3d - failed code: " + code);
Toast.makeText(this@TempActivity, "未能查询到任何地点", Toast.LENGTH_SHORT).show()
}
temp_swipe.isRefreshing = false
}
})
}
.............
}
天气刷新
此功能还是比较简单的,刷新,意思就是重新请求下数据呗,直接调用上方的 searchTempOrRefresh方法即可。不多说了。
调用系统相机拍照
下回分解
心情历程记录
下回分解
历史心情历程查看
下回分解
源码地址:
https://gitee.com/kanecong/weather-test