• 使用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'
    
    • 1
    • 2
    • 3
    • 4
    • 5

    项目根目录build.gradle

    buildscript {
        dependencies {
            classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.1"
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在项目级目录中打开 secrets.properties,然后添加以下代码。将 YOUR_API_KEY 替换为您的 API 密钥

    MAPS_API_KEY=YOUR_API_KEY 
    
    • 1

    在 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}" />
    
    • 1
    • 2
    • 3

    在Application中初始化

    
        // Initialize the SDK
        Places.initialize(getApplicationContext(), apiKey);
    
        // Create a new PlacesClient instance
        //在实际使用的时候调用,初始化时候可以不用这个
        PlacesClient placesClient = Places.createClient(this);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    三、产品需求

    这里需要实现一个在搜索框中输入内容,然后将结果展示出来的功能。如果有内容展示内容,如果没有内容显示空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()
        }
    }
    
    • 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
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102

    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
    • 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
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161

    四、参考链接

    1. Place Sdk for Android:
    2. CancellationToken
    3. PlacesClient
    4. ApiException
    5. place-details
  • 相关阅读:
    json提取-响应报文中是json数组
    Java设计模式之中介者模式
    Android入门第39天-系统设置Configuration类
    STK 12.5.0发布
    Ansible的脚本 --- playbook 剧本
    保障新能源园区安全无忧:可燃气体报警器校准检测的必要性探讨
    软件工程师,全面思考问题很重要
    青云霍秉杰:一文读懂Prometheus长期存储主流方案
    Linux 权限管理
    springboot+安卓app电子阅览室系统毕业设计源码016514
  • 原文地址:https://blog.csdn.net/Mr_Tony/article/details/134042629