Android 实现高德地图之 POI 搜索功能
一、项目背景详细介绍
POI(Point of Interest,兴趣点)搜索功能是移动地图类应用中的核心能力之一。通过 POI 搜索,用户可以快速获取周边的餐饮、酒店、银行、地铁站、医院等各类位置信息。这类功能不仅应用在出行导航场景,还常用于电商(定位最近门店)、政务服务(附近办事大厅)、出行打车(附近上车点)等。
在 Android 平台上,常见的地图 SDK 包括百度地图、高德地图、腾讯地图。高德地图(AMap)在国内覆盖率与准确性都较高,并且开放了丰富的 SDK 接口,其中就包括 POI 搜索功能。
本项目的背景是:在一个 Android 应用中,需要集成高德地图 SDK 并实现一个简洁的 POI 搜索功能。用户输入关键词,例如“咖啡”或者“银行”,系统能够调用高德地图提供的 API 返回符合条件的兴趣点,并以列表或地图标注的形式展示出来。
二、项目需求详细介绍
项目需求如下:
-
集成高德地图 SDK 并正确初始化。
-
提供一个输入框,用户可以输入关键词(例如“餐厅”)。
-
调用高德地图的 POI 搜索接口,根据关键词在指定城市或当前定位城市内进行搜索。
-
将返回的 POI 信息以列表展示,包含:名称、地址、经纬度。
-
点击列表项时,能在地图上标注该 POI。
-
支持清空结果和重新搜索。
扩展需求:
-
支持根据当前位置做周边搜索(例如“我附近的咖啡”)。
-
支持分页加载更多结果。
-
支持用户点击地图标注查看 POI 详情。
三、相关技术详细介绍
实现该功能会用到以下技术点:
-
高德地图 Android SDK
提供地图展示、定位、导航等功能。 -
高德地图搜索服务(POI Search API)
通过com.amap.api.services.poisearch.PoiSearch
提供的接口,可实现关键字搜索、周边搜索、多边形范围搜索等。 -
权限与密钥
-
高德地图需要在官网申请 Key 并在
AndroidManifest.xml
中配置。 -
应用需要申请运行时权限:
ACCESS_FINE_LOCATION
、ACCESS_COARSE_LOCATION
、网络权限。
-
-
UI 控件
-
EditText
用于输入关键词。 -
RecyclerView
或ListView
用于展示 POI 搜索结果。 -
MapView
显示地图与标注。
-
-
异步回调
POI 搜索为异步操作,需要实现PoiSearch.OnPoiSearchListener
接口,在回调中获取结果。
四、实现思路详细介绍
-
在高德开放平台注册开发者账号,申请 Android Key 并配置 SHA1 签名。
-
在 Android 项目中导入高德地图 SDK(通过 Maven 或手动导入)。
-
在布局文件中放置一个
MapView
,一个输入框(EditText
),以及一个结果列表(RecyclerView
)。 -
初始化
MapView
并在 Activity 生命周期中调用onCreate/onResume/onPause/onDestroy
。 -
用户输入关键词并点击搜索按钮时,调用高德地图的 POI 搜索接口:
-
创建
PoiSearch.Query
,设置关键字、搜索类型、搜索城市。 -
创建
PoiSearch
对象并发起搜索。 -
在
onPoiSearched
回调中获取 POI 列表。
-
-
将结果列表绑定到 RecyclerView,展示名称、地址、经纬度。
-
点击列表项时,在地图上清空已有标注并新增一个
Marker
,显示 POI 的位置。
五、完整实现代码
// ======================= 配置文件部分 =======================
// 文件:AndroidManifest.xml (只展示关键配置)
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.amappoisearch">
<!-- 必要权限 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:allowBackup="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<!-- 高德地图 key 配置 -->
<meta-data
android:name="com.amap.api.v2.apikey"
android:value="你申请的key"/>
</application>
</manifest>
// ======================= 布局文件部分 =======================
// 文件:res/layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 输入框和按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<EditText
android:id="@+id/etKeyword"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:hint="请输入搜索关键词"/>
<Button
android:id="@+id/btnSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="搜索"/>
</LinearLayout>
<!-- 地图 -->
<com.amap.api.maps2d.MapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="300dp"/>
<!-- 搜索结果列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvResult"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
// ======================= Java 代码部分 =======================
// 文件:MainActivity.java
package com.example.amappoisearch;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.amap.api.maps2d.AMap;
import com.amap.api.maps2d.CameraUpdateFactory;
import com.amap.api.maps2d.MapView;
import com.amap.api.maps2d.model.BitmapDescriptorFactory;
import com.amap.api.maps2d.model.LatLng;
import com.amap.api.maps2d.model.MarkerOptions;
import com.amap.api.services.core.AMapException;
import com.amap.api.services.core.LatLonPoint;
import com.amap.api.services.poisearch.PoiResult;
import com.amap.api.services.poisearch.PoiSearch;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity implements PoiSearch.OnPoiSearchListener {
private MapView mapView;
private AMap aMap;
private EditText etKeyword;
private Button btnSearch;
private RecyclerView rvResult;
private PoiAdapter adapter;
private List<PoiItemData> poiList = new ArrayList<>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etKeyword = findViewById(R.id.etKeyword);
btnSearch = findViewById(R.id.btnSearch);
rvResult = findViewById(R.id.rvResult);
mapView = findViewById(R.id.mapView);
mapView.onCreate(savedInstanceState);
aMap = mapView.getMap();
rvResult.setLayoutManager(new LinearLayoutManager(this));
adapter = new PoiAdapter(poiList, item -> {
// 点击列表项时,在地图上标注
aMap.clear();
LatLng latLng = new LatLng(item.getLat(), item.getLon());
aMap.addMarker(new MarkerOptions()
.position(latLng)
.title(item.getTitle())
.snippet(item.getSnippet())
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED)));
aMap.moveCamera(CameraUpdateFactory.newLatLngZoom(latLng, 15));
});
rvResult.setAdapter(adapter);
btnSearch.setOnClickListener(v -> doSearch());
}
private void doSearch() {
String keyword = etKeyword.getText().toString().trim();
if (TextUtils.isEmpty(keyword)) {
Toast.makeText(this, "请输入关键词", Toast.LENGTH_SHORT).show();
return;
}
PoiSearch.Query query = new PoiSearch.Query(keyword, "", "北京");
query.setPageSize(10);
query.setPageNum(1);
PoiSearch poiSearch = new PoiSearch(this, query);
poiSearch.setOnPoiSearchListener(this);
poiSearch.searchPOIAsyn();
}
@Override
public void onPoiSearched(PoiResult result, int rCode) {
if (rCode == AMapException.CODE_AMAP_SUCCESS) {
poiList.clear();
if (result != null && result.getPois() != null) {
for (com.amap.api.services.core.PoiItem item : result.getPois()) {
poiList.add(new PoiItemData(
item.getTitle(),
item.getSnippet(),
item.getLatLonPoint().getLatitude(),
item.getLatLonPoint().getLongitude()
));
}
adapter.notifyDataSetChanged();
} else {
Toast.makeText(this, "无搜索结果", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(this, "搜索失败:" + rCode, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onPoiItemSearched(com.amap.api.services.core.PoiItem poiItem, int i) {
}
// 生命周期管理
@Override protected void onResume() { super.onResume(); mapView.onResume(); }
@Override protected void onPause() { super.onPause(); mapView.onPause(); }
@Override protected void onDestroy() { super.onDestroy(); mapView.onDestroy(); }
@Override protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mapView.onSaveInstanceState(outState);
}
}
// ======================= 数据模型 =======================
// 文件:PoiItemData.java
package com.example.amappoisearch;
public class PoiItemData {
private String title;
private String snippet;
private double lat;
private double lon;
public PoiItemData(String title, String snippet, double lat, double lon) {
this.title = title;
this.snippet = snippet;
this.lat = lat;
this.lon = lon;
}
public String getTitle() { return title; }
public String getSnippet() { return snippet; }
public double getLat() { return lat; }
public double getLon() { return lon; }
}
// ======================= RecyclerView 适配器 =======================
// 文件:PoiAdapter.java
package com.example.amappoisearch;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class PoiAdapter extends RecyclerView.Adapter<PoiAdapter.ViewHolder> {
public interface OnItemClickListener {
void onItemClick(PoiItemData item);
}
private List<PoiItemData> data;
private OnItemClickListener listener;
public PoiAdapter(List<PoiItemData> data, OnItemClickListener listener) {
this.data = data;
this.listener = listener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_2, parent, false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
PoiItemData item = data.get(position);
holder.tv1.setText(item.getTitle());
holder.tv2.setText(item.getSnippet());
holder.itemView.setOnClickListener(v -> listener.onItemClick(item));
}
@Override
public int getItemCount() {
return data.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
TextView tv1, tv2;
ViewHolder(View v) {
super(v);
tv1 = v.findViewById(android.R.id.text1);
tv2 = v.findViewById(android.R.id.text2);
}
}
}
六、代码详细解读
-
MainActivity.java
-
初始化地图
MapView
和AMap
。 -
用户点击搜索按钮后,构造
PoiSearch.Query
并调用异步搜索。 -
实现
onPoiSearched
回调,处理搜索结果。
-
-
PoiItemData.java
-
简单的数据类,存储 POI 的名称、地址、经纬度。
-
-
PoiAdapter.java
-
RecyclerView 适配器,将搜索结果展示为列表,并提供点击事件回调。
-
七、项目详细总结
本项目完成了从 集成高德地图 SDK → 输入关键词 → 发起 POI 搜索 → 展示结果 → 地图标注 的完整流程。整体结构清晰,UI 简单直观,能够满足大多数场景下的 POI 搜索需求。
八、项目常见问题及解答
-
为什么搜索失败?
-
确认高德 Key 配置是否正确,SHA1 签名是否与官网一致。
-
确认是否开启了网络权限。
-
-
如何实现分页搜索?
-
修改
Query.setPageNum(pageIndex)
,在列表滑动到底部时加载下一页。
-
-
如何做周边搜索?
-
使用
PoiSearch.SearchBound
,传入一个LatLonPoint
和半径即可。
-
-
如何展示多个结果的标注?
-
在回调中遍历 POI 列表,调用
aMap.addMarker()
添加多个标记点。
-
九、扩展方向与性能优化
-
分页与上拉加载更多:提升用户体验,避免一次性加载过多数据。
-
结合定位功能:根据用户当前位置搜索附近 POI。
-
地图聚合展示:当结果较多时,使用点聚合技术优化展示。
-
POI 详情页:点击结果可跳转到详情页展示电话、营业时间、评分等。
-
缓存与离线支持:对常用搜索结果做缓存,提升响应速度。