使用Google的地点自动补全功能

一、前言

在进行海外开发时候需要使用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)
        }
    }
}

四、参考链接

  1. Place Sdk for Android:
  2. CancellationToken
  3. PlacesClient
  4. ApiException
  5. place-details

你可能感兴趣的:(Android,第三方开放平台,android)