Android实现高德地图之poi搜索功能(附带源码)

Android 实现高德地图之 POI 搜索功能

一、项目背景详细介绍

POI(Point of Interest,兴趣点)搜索功能是移动地图类应用中的核心能力之一。通过 POI 搜索,用户可以快速获取周边的餐饮、酒店、银行、地铁站、医院等各类位置信息。这类功能不仅应用在出行导航场景,还常用于电商(定位最近门店)、政务服务(附近办事大厅)、出行打车(附近上车点)等。

在 Android 平台上,常见的地图 SDK 包括百度地图、高德地图、腾讯地图。高德地图(AMap)在国内覆盖率与准确性都较高,并且开放了丰富的 SDK 接口,其中就包括 POI 搜索功能。

本项目的背景是:在一个 Android 应用中,需要集成高德地图 SDK 并实现一个简洁的 POI 搜索功能。用户输入关键词,例如“咖啡”或者“银行”,系统能够调用高德地图提供的 API 返回符合条件的兴趣点,并以列表或地图标注的形式展示出来。


二、项目需求详细介绍

项目需求如下:

  1. 集成高德地图 SDK 并正确初始化。

  2. 提供一个输入框,用户可以输入关键词(例如“餐厅”)。

  3. 调用高德地图的 POI 搜索接口,根据关键词在指定城市或当前定位城市内进行搜索。

  4. 将返回的 POI 信息以列表展示,包含:名称、地址、经纬度。

  5. 点击列表项时,能在地图上标注该 POI。

  6. 支持清空结果和重新搜索。

扩展需求:

  • 支持根据当前位置做周边搜索(例如“我附近的咖啡”)。

  • 支持分页加载更多结果。

  • 支持用户点击地图标注查看 POI 详情。


三、相关技术详细介绍

实现该功能会用到以下技术点:

  1. 高德地图 Android SDK
    提供地图展示、定位、导航等功能。

  2. 高德地图搜索服务(POI Search API)
    通过 com.amap.api.services.poisearch.PoiSearch 提供的接口,可实现关键字搜索、周边搜索、多边形范围搜索等。

  3. 权限与密钥

    • 高德地图需要在官网申请 Key 并在 AndroidManifest.xml 中配置。

    • 应用需要申请运行时权限:ACCESS_FINE_LOCATIONACCESS_COARSE_LOCATION、网络权限。

  4. UI 控件

    • EditText 用于输入关键词。

    • RecyclerViewListView 用于展示 POI 搜索结果。

    • MapView 显示地图与标注。

  5. 异步回调
    POI 搜索为异步操作,需要实现 PoiSearch.OnPoiSearchListener 接口,在回调中获取结果。


四、实现思路详细介绍

  1. 在高德开放平台注册开发者账号,申请 Android Key 并配置 SHA1 签名。

  2. 在 Android 项目中导入高德地图 SDK(通过 Maven 或手动导入)。

  3. 在布局文件中放置一个 MapView,一个输入框(EditText),以及一个结果列表(RecyclerView)。

  4. 初始化 MapView 并在 Activity 生命周期中调用 onCreate/onResume/onPause/onDestroy

  5. 用户输入关键词并点击搜索按钮时,调用高德地图的 POI 搜索接口:

    • 创建 PoiSearch.Query,设置关键字、搜索类型、搜索城市。

    • 创建 PoiSearch 对象并发起搜索。

    • onPoiSearched 回调中获取 POI 列表。

  6. 将结果列表绑定到 RecyclerView,展示名称、地址、经纬度。

  7. 点击列表项时,在地图上清空已有标注并新增一个 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

    • 初始化地图 MapViewAMap

    • 用户点击搜索按钮后,构造 PoiSearch.Query 并调用异步搜索。

    • 实现 onPoiSearched 回调,处理搜索结果。

  • PoiItemData.java

    • 简单的数据类,存储 POI 的名称、地址、经纬度。

  • PoiAdapter.java

    • RecyclerView 适配器,将搜索结果展示为列表,并提供点击事件回调。


七、项目详细总结

本项目完成了从 集成高德地图 SDK → 输入关键词 → 发起 POI 搜索 → 展示结果 → 地图标注 的完整流程。整体结构清晰,UI 简单直观,能够满足大多数场景下的 POI 搜索需求。


八、项目常见问题及解答

  1. 为什么搜索失败?

    • 确认高德 Key 配置是否正确,SHA1 签名是否与官网一致。

    • 确认是否开启了网络权限。

  2. 如何实现分页搜索?

    • 修改 Query.setPageNum(pageIndex),在列表滑动到底部时加载下一页。

  3. 如何做周边搜索?

    • 使用 PoiSearch.SearchBound,传入一个 LatLonPoint 和半径即可。

  4. 如何展示多个结果的标注?

    • 在回调中遍历 POI 列表,调用 aMap.addMarker() 添加多个标记点。


九、扩展方向与性能优化

  1. 分页与上拉加载更多:提升用户体验,避免一次性加载过多数据。

  2. 结合定位功能:根据用户当前位置搜索附近 POI。

  3. 地图聚合展示:当结果较多时,使用点聚合技术优化展示。

  4. POI 详情页:点击结果可跳转到详情页展示电话、营业时间、评分等。

  5. 缓存与离线支持:对常用搜索结果做缓存,提升响应速度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值