在进行海外开发时候需要使用google地图,这里对其中的地点自动补全功能开发进行记录。这里着重于代码开发,对于key的申请和配置不予记录。
app文件夹下面的build.gradle
plugins {
// ...
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin'
}
implementation 'com.google.android.libraries.places:places:3.0.0'
项目根目录build.gradle
buildscript {
dependencies {
classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
}
}
在项目级目录中打开 secrets.properties,然后添加以下代码。将 YOUR_API_KEY 替换为您的 API 密钥
MAPS_API_KEY=YOUR_API_KEY
在 AndroidManifest.xml 文件中,定位到 com.google.android.geo.API_KEY 并按如下所示更新 android:value attribute:
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="${MAPS_API_KEY}" />
在Application中初始化
// Initialize the SDK
Places.initialize(getApplicationContext(), apiKey);
// Create a new PlacesClient instance
//在实际使用的时候调用,初始化时候可以不用这个
PlacesClient placesClient = Places.createClient(this);
这里需要实现一个在搜索框中输入内容,然后将结果展示出来的功能。如果有内容展示内容,如果没有内容显示空UI,网络错误显示错误UI。删除内容后,将搜索结果的UI隐藏,展示另外一种UI。点击搜索结果获取地理位置的经纬度
程序由Fragment、ViewModel、xml组成。为了节约文章内容,只给出核心代码,布局文件不再给出
SearchViewModel.kt
class SearchViewModel: ViewModel(){
val predictions = MutableLiveData<MutableList<AutocompletePrediction>>()
val placeLiveData = MutableLiveData<Place>()
val errorLiveData = MutableLiveData<ApiException>()
private val cancelTokenSource = CancellationTokenSource()
private var placesClient: PlacesClient ?= null
private val TAG = "SearchViewModel"
enum class QueryState{
LOADING,
EMPTY,
NET_ERROR,
SUCCESS
}
fun createPlaceClient(context: Context){
try {
placesClient = Places.createClient(context)
}catch (e: Exception){
}
}
private var token: AutocompleteSessionToken ?= null
fun searchCity(query: String){
//参考代码: https://developers.google.com/android/reference/com/google/android/gms/tasks/CancellationToken
//参考代码: https://developers.google.com/maps/documentation/places/android-sdk/place-details?hl=zh-cn
//参考代码: https://developers.google.com/maps/documentation/places/android-sdk/reference/com/google/android/libraries/places/api/net/PlacesClient
//ApiException: https://developers.google.com/android/reference/com/google/android/gms/common/api/ApiException
if(null == placesClient){
errorLiveData.postValue(ApiException(Status.RESULT_INTERNAL_ERROR))
return
}
token = AutocompleteSessionToken.newInstance()
val request =
FindAutocompletePredictionsRequest.builder()
.setTypesFilter(listOf(PlaceTypes.CITIES))
.setSessionToken(token)
.setCancellationToken(cancelTokenSource.token)
.setQuery(query)
.build()
placesClient?.findAutocompletePredictions(request)
?.addOnSuccessListener { response: FindAutocompletePredictionsResponse ->
// for (prediction in response.autocompletePredictions) {
// Log.i(TAG, prediction.placeId)
// Log.i(TAG, prediction.getPrimaryText(null).toString())
// }
predictions.postValue(response.autocompletePredictions.toMutableList())
}?.addOnFailureListener { exception: Exception? ->
if (exception is ApiException) {
// Log.e(TAG, "Place not found:code--> ${exception.statusCode}-->message:${exception.message}")
exception?.let {
errorLiveData.postValue(it)
}
}else{
errorLiveData.postValue(ApiException(Status.RESULT_INTERNAL_ERROR))
}
}
}
//搜索城市详情
fun requestCityDetails(position: Int){
if(null == placesClient){
errorLiveData.postValue(ApiException(Status.RESULT_INTERNAL_ERROR))
return
}
val prediction = predictions.value?.get(position)
if(null == prediction){
errorLiveData.postValue(ApiException(Status.RESULT_INTERNAL_ERROR))
return
}
val placeId = prediction.placeId
val placeFields = listOf(Place.Field.LAT_LNG, Place.Field.NAME)
val request = FetchPlaceRequest
.builder(placeId, placeFields)
.setCancellationToken(cancelTokenSource.token)
.setSessionToken(token)
.build()
placesClient?.fetchPlace(request)
?.addOnSuccessListener { response: FetchPlaceResponse ->
val place = response.place
// Log.i(TAG, "Place found: ${place.name}-->latitude:${place.latLng?.latitude}--->longitude:${place.latLng?.longitude}")
placeLiveData.postValue(place)
}?.addOnFailureListener { exception: Exception ->
if (exception is ApiException) {
// Log.e(TAG, "Place not found: ${exception.message}")
exception?.let {
errorLiveData.postValue(it)
}
}else{
errorLiveData.postValue(ApiException(Status.RESULT_INTERNAL_ERROR))
}
}
}
fun cancelQuery(){
cancelTokenSource.cancel()
}
override fun onCleared() {
super.onCleared()
cancelQuery()
}
}
SearchFragment.kt
class SearchFragment: Fragment(){
private val searchCityResultAdapter = SearchCityResultAdapter()
private val textWatch = CustomTextWatch()
private val handler = object : Handler(Looper.getMainLooper()){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
when(msg.what){
customEditActionListener.msgAction -> {
val actionContent = msg.obj as? CharSequence ?: return
val query = actionContent.toString()
if(TextUtils.isEmpty(query)){
return
}
switchSearchUi(true)
viewModel.searchCity(query)
}
textWatch.msgAction -> {
val actionContent = msg.obj as? Editable
if (TextUtils.isEmpty(actionContent)){
switchSearchUi(false)
viewModel.cancelQuery()
}
}
}
}
}
private fun initRecycleView(){
....
searchCityResultAdapter.setOnItemClickListener { _, _, position ->
viewModel.requestCityDetails(position)
switchSearchUi(false)
}
}
private fun initListener(){
customEditActionListener.bindHandler(handler)
binding.etSearchInput.setOnEditorActionListener(customEditActionListener)
textWatch.bindHandler(handler)
binding.etSearchInput.addTextChangedListener(textWatch)
....
}
private fun switchSearchUi(isShowSearchUi: Boolean){
if (isShowSearchUi){
searchStateUi(RecommendViewModel.QueryState.LOADING)
binding.nsvRecommend.visibility = View.GONE
}else{
binding.layoutSearchResult.root.visibility = View.GONE
binding.nsvRecommend.visibility = View.VISIBLE
}
}
private fun initObserver() {
...
viewModel.predictions.observe(this){
if (it.isEmpty()){
searchStateUi(RecommendViewModel.QueryState.EMPTY)
}else{
searchStateUi(RecommendViewModel.QueryState.SUCCESS)
searchCityResultAdapter.setNewInstance(it)
}
}
viewModel.placeLiveData.observe(this){
addCity(it)
}
viewModel.errorLiveData.observe(this){
AddCityFailedUtils.trackLocationFailure("search",it.message.toString())
Log.i("TAG", it.message ?: "")
if(it.status == Status.RESULT_TIMEOUT){
searchStateUi(RecommendViewModel.QueryState.NET_ERROR)
}else{
searchStateUi(RecommendViewModel.QueryState.EMPTY)
}
}
...
}
//查询结果状态
private fun searchStateUi(state: RecommendViewModel.QueryState){
val searchResultBinding = binding.layoutSearchResult
searchResultBinding.root.visibility = View.VISIBLE
when(state){
RecommendViewModel.QueryState.LOADING -> {
searchResultBinding.lottieLoading.visibility = View.VISIBLE
searchResultBinding.rvSearchResult.visibility = View.GONE
searchResultBinding.ivError.visibility = View.GONE
}
RecommendViewModel.QueryState.EMPTY -> {
searchResultBinding.ivError.setImageResource(R.drawable.no_positioning)
searchResultBinding.lottieLoading.visibility = View.GONE
searchResultBinding.rvSearchResult.visibility = View.GONE
searchResultBinding.ivError.visibility = View.VISIBLE
}
RecommendViewModel.QueryState.NET_ERROR -> {
searchResultBinding.ivError.setImageResource(R.drawable.no_network)
searchResultBinding.lottieLoading.visibility = View.GONE
searchResultBinding.rvSearchResult.visibility = View.GONE
searchResultBinding.ivError.visibility = View.VISIBLE
}
RecommendViewModel.QueryState.SUCCESS -> {
searchResultBinding.lottieLoading.visibility = View.VISIBLE
searchResultBinding.rvSearchResult.visibility = View.GONE
searchResultBinding.ivError.visibility = View.GONE
}
else -> {
}
}
}
override fun onDestroy() {
super.onDestroy()
binding.etSearchInput.removeTextChangedListener(textWatch)
handler.removeCallbacksAndMessages(null)
}
inner class CustomEditTextActionListener: TextView.OnEditorActionListener{
private var mHandler: Handler ?= null
val msgAction = 10
fun bindHandler(handler: Handler){
mHandler = handler
}
override fun onEditorAction(v: TextView, actionId: Int, event: KeyEvent?): Boolean {
if(actionId == EditorInfo.IME_ACTION_SEARCH){
hiddenImme(v)
val message = Message.obtain()
message.what = msgAction
message.obj = v.text
mHandler?.sendMessage(message)
return true
}
return false
}
private fun hiddenImme(view: View){
//隐藏软键盘
val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (imm.isActive) {
imm.hideSoftInputFromWindow(view.applicationWindowToken, 0)
}
}
}
inner class CustomTextWatch: TextWatcher{
private var mHandler: Handler ?= null
val msgAction = 11
fun bindHandler(handler: Handler){
mHandler = handler
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
}
override fun afterTextChanged(s: Editable?) {
val message = Message.obtain()
message.what = msgAction
message.obj = s
mHandler?.sendMessage(message)
}
}
}