当前位置: 首页 > article >正文

使用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

http://www.kler.cn/a/105685.html

相关文章:

  • kvm-dmesg:从宿主机窥探虚拟机内核dmesg日志
  • Label-studio-ml-backend 和YOLOV8 YOLO11自动化标注,目标检测,实例分割,图像分类,关键点估计,视频跟踪
  • ArkTS组件结构和状态管理
  • K8S资源限制之resources
  • (33)iptables设置防火墙策略常用命令(docker环境、非docker环境)
  • 【HCIP]——OSPF综合实验
  • BC v1.2充电规范
  • 【开源】基于SpringBoot的农村物流配送系统的设计和实现
  • 【STM32】HAL库——串口中断只接收到两个字符
  • 【Java】电子病历编辑器源码(云端SaaS服务)
  • 通过cpolar分享本地电脑上有趣的照片:部署piwigo网页
  • 【Zero to One系列】微服务Hystrix的熔断器集成
  • 如何在用pip配置文件设置HTTP爬虫IP
  • MySQL创建定时任务定时执行sql
  • EasyExcel使用方式(包含导出图片)
  • 【C++】list的介绍及使用 | 模拟实现list(万字详解)
  • Doceker-compose——容器群集编排管理工具
  • ERP管理系统的运作流程是怎样的?
  • 【Axure教程】中继器制作树元件
  • 10月25日,每日信息差
  • IntelliJ IDEA 2023.2正式发布,新UI和Profiler转正
  • LUCEDA IPKISS------Definition Properties 表格查询
  • iOS调试技巧——使用Python 自定义LLDB
  • 关于癌细胞MR的几种类型,T1,T2,DCE,DWI,ADC
  • 【开源】基于SpringBoot的高校学院网站的设计和实现
  • vue使用.filter方法检索数组中指定时间段内的数据