[鸿蒙2025领航者闯关]HarmonyOS中开发高德地图第六篇:POI搜索功能

第六篇:POI搜索功能

本篇教程将学习如何使用高德地图的POI(兴趣点)搜索功能,包括关键字搜索、周边搜索、ID搜索等。

学习目标

  • 掌握POI关键字搜索
  • 实现周边搜索功能
  • 处理搜索结果并在地图上展示
  • 理解输入提示(InputTips)功能

1. POI搜索类型

搜索类型 适用场景 示例
关键字搜索 按名称搜索某类POI 搜索"星巴克"
周边搜索 搜索指定位置附近的POI 当前位置3公里内的餐厅
多边形搜索 搜索指定区域内的POI 某个行政区内的银行
ID搜索 按POI ID查询详情 获取某个POI的详细信息

2. 核心类说明

类名 说明
PoiSearch POI搜索核心类
PoiQuery 搜索条件配置
PoiResult 搜索结果
PoiItem 单个POI信息
OnPoiSearchListener 搜索回调接口

3. 完整代码示例

创建文件 entry/src/main/ets/pages/Demo05_PoiSearch.ets:

typescript 复制代码
import {
  AMap,
  MapView,
  MapViewComponent,
  MapViewManager,
  MapViewCreateCallback,
  CameraUpdateFactory,
  LatLng,
  Marker,
  MarkerOptions,
  BitmapDescriptorFactory,
  LatLngBounds
} from '@amap/amap_lbs_map3d';
import {
  PoiSearch,
  PoiQuery,
  PoiResult,
  PoiItem,
  OnPoiSearchListener,
  AMapException,
  LatLonPoint,
  PoiSearchBound
} from '@amap/amap_lbs_search';
import { inputMethod } from '@kit.IMEKit';

const MAP_VIEW_NAME = 'PoiSearchDemo';

/**
 * POI搜索类型
 */
type SearchType = 'keyword' | 'around';

@Entry
@Component
struct Demo05_PoiSearch {
  private mapView: MapView | undefined = undefined;
  private aMap: AMap | undefined = undefined;
  private poiSearch: PoiSearch | undefined = undefined;
  private poiMarkers: Marker[] = [];
  
  @State isMapReady: boolean = false;
  @State keyword: string = '餐厅';
  @State city: string = '北京';
  @State searchType: SearchType = 'keyword';
  @State isSearching: boolean = false;
  @State searchResult: string = '';
  @State poiList: PoiItem[] = [];
  @State currentPage: number = 0;
  @State totalPages: number = 0;
  
  // 周边搜索中心点
  private searchCenter: LatLonPoint = new LatLonPoint(39.909187, 116.397451);
  private searchRadius: number = 3000; // 3公里

  /**
   * POI搜索回调
   */
  private poiSearchListener: OnPoiSearchListener = {
    onPoiSearched: (result: PoiResult | undefined, errorCode: number) => {
      this.isSearching = false;
      
      if (errorCode === AMapException.CODE_AMAP_SUCCESS) {
        if (result) {
          const pois = result.getPois();
          if (pois && pois.length > 0) {
            this.poiList = pois as PoiItem[];
            this.currentPage = result.getQuery()?.getPageNum() || 0;
            this.totalPages = result.getPageCount();
            this.searchResult = `找到 ${pois.length} 个结果 (第${this.currentPage + 1}/${this.totalPages}页)`;
            
            // 清除旧标记并添加新标记
            this.clearPoiMarkers();
            this.addPoiMarkersToMap();
            
            // 调整地图视野
            this.fitMapBounds();
          } else {
            this.searchResult = '未找到相关结果';
            this.poiList = [];
            this.clearPoiMarkers();
          }
        }
      } else {
        this.searchResult = `搜索失败: 错误码 ${errorCode}`;
        console.error('[PoiSearch] Search failed:', errorCode);
      }
    },
    
    onPoiItemSearched: (poiItem: PoiItem | undefined, errorCode: number) => {
      // ID搜索回调
      if (errorCode === AMapException.CODE_AMAP_SUCCESS && poiItem) {
        console.info('[PoiSearch] POI detail:', poiItem.getTitle());
      }
    }
  };

  private mapViewCreateCallback: MapViewCreateCallback = 
    (mapview: MapView | undefined, mapViewName: string | undefined) => {
      if (!mapview || mapViewName !== MAP_VIEW_NAME) return;

      this.mapView = mapview;
      this.mapView.onCreate();
      
      this.mapView.getMapAsync((map: AMap) => {
        this.aMap = map;
        this.isMapReady = true;
        
        // 移动到北京
        const beijing = new LatLng(39.909187, 116.397451);
        map.moveCamera(CameraUpdateFactory.newLatLngZoom(beijing, 13));
        
        // 启用控件
        map.getUiSettings()?.setZoomControlsEnabled(true);
        
        // 地图点击设置搜索中心
        map.setOnMapClickListener((point: LatLng) => {
          if (this.searchType === 'around') {
            this.searchCenter = new LatLonPoint(point.latitude, point.longitude);
            this.searchResult = `已设置搜索中心: ${point.latitude.toFixed(4)}, ${point.longitude.toFixed(4)}`;
          }
        });
      });
    };

  /**
   * 执行关键字搜索
   */
  private doKeywordSearch(): void {
    if (!this.poiSearch || !this.keyword.trim()) {
      this.searchResult = '请输入搜索关键字';
      return;
    }
    
    // 收起键盘
    inputMethod.getController().stopInputSession();
    
    this.isSearching = true;
    this.searchResult = '搜索中...';
    
    // 创建搜索条件
    const query = new PoiQuery(this.keyword, '', this.city);
    query.setPageSize(20);       // 每页数量
    query.setPageNum(0);         // 页码(从0开始)
    
    // 设置查询并执行搜索
    this.poiSearch.setQuery(query);
    this.poiSearch.searchPOIAsyn();
    
    console.info('[PoiSearch] Keyword search:', this.keyword, 'in', this.city);
  }

  /**
   * 执行周边搜索
   */
  private doAroundSearch(): void {
    if (!this.poiSearch || !this.keyword.trim()) {
      this.searchResult = '请输入搜索关键字';
      return;
    }
    
    inputMethod.getController().stopInputSession();
    
    this.isSearching = true;
    this.searchResult = '搜索中...';
    
    // 创建搜索条件
    const query = new PoiQuery(this.keyword, '', '');
    query.setPageSize(20);
    query.setPageNum(0);
    
    // 设置周边搜索范围
    const bound = new PoiSearchBound(this.searchCenter, this.searchRadius);
    query.setBound(bound);
    
    this.poiSearch.setQuery(query);
    this.poiSearch.searchPOIAsyn();
    
    console.info('[PoiSearch] Around search:', this.keyword, 'radius:', this.searchRadius);
  }

  /**
   * 加载下一页
   */
  private loadNextPage(): void {
    if (!this.poiSearch || this.currentPage >= this.totalPages - 1) {
      return;
    }
    
    this.isSearching = true;
    this.currentPage++;
    
    const query = this.poiSearch.getQuery();
    if (query) {
      query.setPageNum(this.currentPage);
      this.poiSearch.setQuery(query);
      this.poiSearch.searchPOIAsyn();
    }
  }

  /**
   * 在地图上添加POI标记
   */
  private addPoiMarkersToMap(): void {
    if (!this.aMap) return;
    
    for (let i = 0; i < this.poiList.length; i++) {
      const poi = this.poiList[i];
      const latLonPoint = poi.getLatLonPoint();
      
      if (latLonPoint) {
        const options = new MarkerOptions();
        options.setPosition(new LatLng(latLonPoint.getLatitude(), latLonPoint.getLongitude()));
        options.setTitle(poi.getTitle() || '');
        options.setSnippet(poi.getSnippet() || '');
        
        // 使用不同颜色标记
        const hue = i < 5 ? BitmapDescriptorFactory.HUE_RED : BitmapDescriptorFactory.HUE_BLUE;
        options.setIcon(BitmapDescriptorFactory.defaultMarker(hue));
        options.setZIndex(10);
        
        const marker = this.aMap.addMarker(options);
        if (marker) {
          this.poiMarkers.push(marker);
        }
      }
    }
  }

  /**
   * 清除POI标记
   */
  private clearPoiMarkers(): void {
    for (const marker of this.poiMarkers) {
      marker.remove();
    }
    this.poiMarkers = [];
  }

  /**
   * 调整地图视野以显示所有标记
   */
  private fitMapBounds(): void {
    if (!this.aMap || this.poiList.length === 0) return;
    
    // 计算边界
    let minLat = 90, maxLat = -90, minLng = 180, maxLng = -180;
    
    for (const poi of this.poiList) {
      const point = poi.getLatLonPoint();
      if (point) {
        const lat = point.getLatitude();
        const lng = point.getLongitude();
        minLat = Math.min(minLat, lat);
        maxLat = Math.max(maxLat, lat);
        minLng = Math.min(minLng, lng);
        maxLng = Math.max(maxLng, lng);
      }
    }
    
    if (minLat < maxLat && minLng < maxLng) {
      const southwest = new LatLng(minLat, minLng);
      const northeast = new LatLng(maxLat, maxLng);
      const bounds = new LatLngBounds(southwest, northeast);
      
      this.aMap.animateCamera(
        CameraUpdateFactory.newLatLngBounds(bounds, 50),
        500
      );
    }
  }

  /**
   * 点击POI项,移动到该位置
   */
  private onPoiItemClick(poi: PoiItem): void {
    const point = poi.getLatLonPoint();
    if (point && this.aMap) {
      const latLng = new LatLng(point.getLatitude(), point.getLongitude());
      this.aMap.animateCamera(
        CameraUpdateFactory.newLatLngZoom(latLng, 16),
        500
      );
      
      // 显示对应标记的InfoWindow
      for (const marker of this.poiMarkers) {
        const pos = marker.getPosition();
        if (Math.abs(pos.latitude - point.getLatitude()) < 0.0001 &&
            Math.abs(pos.longitude - point.getLongitude()) < 0.0001) {
          marker.showInfoWindow();
          break;
        }
      }
    }
  }

  aboutToAppear(): void {
    MapViewManager.getInstance()
      .registerMapViewCreatedCallback(this.mapViewCreateCallback);
    
    // 初始化POI搜索
    const context = getContext(this);
    this.poiSearch = new PoiSearch(context, undefined);
    this.poiSearch.setOnPoiSearchListener(this.poiSearchListener);
  }

  aboutToDisappear(): void {
    this.clearPoiMarkers();
    
    MapViewManager.getInstance()
      .unregisterMapViewCreatedCallback(this.mapViewCreateCallback);
    
    if (this.mapView) {
      this.mapView.onDestroy();
      this.mapView = undefined;
      this.aMap = undefined;
    }
  }

  build() {
    Column() {
      // 标题栏
      Row() {
        Text('POI搜索')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
      }
      .width('100%')
      .height(50)
      .padding({ left: 16 })
      .backgroundColor('#9C27B0')

      // 搜索栏
      Column() {
        // 搜索类型选择
        Row() {
          Row() {
            Radio({ value: 'keyword', group: 'searchType' })
              .checked(this.searchType === 'keyword')
              .onChange((isChecked: boolean) => {
                if (isChecked) this.searchType = 'keyword';
              })
            Text('关键字搜索')
              .fontSize(14)
              .margin({ left: 4 })
          }
          .margin({ right: 20 })
          
          Row() {
            Radio({ value: 'around', group: 'searchType' })
              .checked(this.searchType === 'around')
              .onChange((isChecked: boolean) => {
                if (isChecked) this.searchType = 'around';
              })
            Text('周边搜索')
              .fontSize(14)
              .margin({ left: 4 })
          }
        }
        .width('100%')
        .margin({ bottom: 8 })

        // 搜索输入
        Row() {
          TextInput({ text: this.keyword, placeholder: '输入关键字' })
            .layoutWeight(1)
            .height(40)
            .onChange((value: string) => { this.keyword = value; })
          
          if (this.searchType === 'keyword') {
            TextInput({ text: this.city, placeholder: '城市' })
              .width(80)
              .height(40)
              .margin({ left: 8 })
              .onChange((value: string) => { this.city = value; })
          }
          
          Button('搜索')
            .height(40)
            .margin({ left: 8 })
            .enabled(!this.isSearching)
            .onClick(() => {
              if (this.searchType === 'keyword') {
                this.doKeywordSearch();
              } else {
                this.doAroundSearch();
              }
            })
        }
        .width('100%')

        // 搜索结果提示
        Text(this.searchResult)
          .fontSize(12)
          .fontColor('#666')
          .margin({ top: 8 })
          .width('100%')
      }
      .padding(12)
      .backgroundColor('#f5f5f5')

      // 地图和结果列表
      Row() {
        // 地图
        Stack() {
          MapViewComponent({ mapViewName: MAP_VIEW_NAME })
            .width('100%')
            .height('100%')
          
          if (this.searchType === 'around') {
            Text('点击地图设置搜索中心')
              .fontSize(11)
              .fontColor('#fff')
              .backgroundColor('rgba(0,0,0,0.6)')
              .padding(6)
              .borderRadius(4)
              .position({ x: 10, y: 10 })
          }
        }
        .layoutWeight(1)
        .height('100%')

        // 结果列表
        if (this.poiList.length > 0) {
          Column() {
            Text('搜索结果')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .padding(8)
              .width('100%')
              .backgroundColor('#e0e0e0')
            
            List() {
              ForEach(this.poiList, (poi: PoiItem, index: number) => {
                ListItem() {
                  Column() {
                    Text(`${index + 1}. ${poi.getTitle() || '未知'}`)
                      .fontSize(13)
                      .fontColor('#333')
                      .maxLines(1)
                      .textOverflow({ overflow: TextOverflow.Ellipsis })
                    Text(poi.getSnippet() || '')
                      .fontSize(11)
                      .fontColor('#999')
                      .maxLines(1)
                      .textOverflow({ overflow: TextOverflow.Ellipsis })
                      .margin({ top: 2 })
                  }
                  .width('100%')
                  .alignItems(HorizontalAlign.Start)
                  .padding(8)
                }
                .onClick(() => this.onPoiItemClick(poi))
              })
            }
            .layoutWeight(1)
            .divider({ strokeWidth: 1, color: '#eee' })
            
            // 加载更多按钮
            if (this.currentPage < this.totalPages - 1) {
              Button('加载更多')
                .width('100%')
                .height(36)
                .fontSize(12)
                .enabled(!this.isSearching)
                .onClick(() => this.loadNextPage())
            }
          }
          .width(150)
          .height('100%')
          .backgroundColor(Color.White)
        }
      }
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
  }
}

4. PoiItem 常用方法

typescript 复制代码
interface PoiItem {
  // 获取POI名称
  getTitle(): string;
  
  // 获取POI地址
  getSnippet(): string;
  
  // 获取POI ID
  getPoiId(): string;
  
  // 获取坐标
  getLatLonPoint(): LatLonPoint;
  
  // 获取电话
  getTel(): string;
  
  // 获取POI类型
  getTypeDes(): string;
  
  // 获取距离(周边搜索时有效)
  getDistance(): number;
  
  // 获取行政区编码
  getAdCode(): string;
  
  // 获取省份
  getProvinceName(): string;
  
  // 获取城市
  getCityName(): string;
  
  // 获取区县
  getAdName(): string;
}

5. 搜索条件配置

5.1 PoiQuery 常用设置

typescript 复制代码
const query = new PoiQuery(keyword, type, city);

// 设置每页结果数量(1-50)
query.setPageSize(20);

// 设置页码(从0开始)
query.setPageNum(0);

// 设置搜索类型(可选,如"餐饮服务|购物服务")
query.setTypes('餐饮服务');

// 设置是否返回扩展信息(all或base)
query.setExtensions('all');

// 设置周边搜索范围
query.setBound(new PoiSearchBound(centerPoint, radius));

// 设置返回语言
query.setQueryLanguage(ServiceSettings.CHINESE);

5.2 常用POI类型

类型编码 说明
餐饮服务 餐厅、快餐、咖啡厅等
购物服务 商场、超市、便利店等
生活服务 银行、邮局、洗衣店等
医疗保健服务 医院、诊所、药店等
住宿服务 酒店、宾馆、民宿等
交通设施服务 停车场、加油站、公交站等

6. 输入提示(InputTips)

用于实现搜索框的自动补全功能:

typescript 复制代码
import { Inputtips, InputtipsQuery, OnInputtipsListener, Tip } from '@amap/amap_lbs_search';

// 创建InputTips
const inputTips = new Inputtips(context);

// 设置回调
inputTips.setInputtipsListener({
  onGetInputtips: (tips: Tip[], errorCode: number) => {
    if (errorCode === AMapException.CODE_AMAP_SUCCESS) {
      tips.forEach(tip => {
        console.log(tip.getName(), tip.getAddress());
      });
    }
  }
});

// 执行查询
const query = new InputtipsQuery('星巴克', '北京');
inputTips.requestInputtips(query);

7. 实用技巧

7.1 处理搜索建议城市

当在某城市搜索不到结果时,API会返回推荐城市:

typescript 复制代码
onPoiSearched: (result: PoiResult | undefined, errorCode: number) => {
  if (result) {
    const suggestions = result.getSearchSuggestionCitys();
    if (suggestions && suggestions.length > 0) {
      suggestions.forEach(city => {
        console.log('推荐城市:', city.getCityName());
      });
    }
  }
}

7.2 搜索特定类型

typescript 复制代码
// 只搜索餐饮类POI
const query = new PoiQuery('', '餐饮服务', '北京');

本篇小结

本篇教程我们学习了:

  • ✅ POI关键字搜索实现
  • ✅ 周边搜索功能
  • ✅ 搜索结果的解析和展示
  • ✅ 分页加载更多结果
  • ✅ 输入提示功能

下一篇我们将学习地理编码与逆地理编码。

班级

https://developer.huawei.com/consumer/cn/training/classDetail/fd34ff9286174e848d34cde7f512ce22?type=1%3Fha_source%3Dhmosclass\&ha_sourceId=89000248

源码地址

https://gitcode.com/daleishen/gaodehmjiaocheng.git

相关推荐
aqi001 分钟前
鸿蒙版本的电子书阅读APP开放源码啦
android·华为·ai编程·harmonyos·鸿蒙
xq95274 小时前
HDC Assistant 插件横空出世
harmonyos
OH_TPC11 小时前
【鸿蒙优选三方库】@ohos/aki:一行代码让 ArkTS 调 C++
harmonyos
wenyiyiyiyiyi12 小时前
我用鸿蒙 ArkTS 写了个自己天天用的备忘录:待办、课表和长期目标,全塞进一块桌面卡片
harmonyos
承渊政道15 小时前
鸿蒙设备远控电脑实测:ToDesk蓝牙键鼠、双模式鼠标、跨端剪贴板,远控电脑更顺手了
计算机外设·电脑·harmonyos·远程工作·todesk
CV工程师丁Sir17 小时前
ArkWeb 手记 04|DevTools 调试与缓存清理
java·spring·缓存·harmonyos
程序猿追1 天前
HarmonyOS 6 上做个极简浏览器:Web 组件 + 前进后退 + 加载进度
前端·华为·harmonyos
程序猿追1 天前
HarmonyOS 6 音视频实战:用 AVPlayer 做一个本地音乐播放器
华为·音视频·harmonyos
威哥爱编程1 天前
HarmonyOS 7 多设备 UX 自动检测实战:AppAnalyzer 多设备体检 + 一多工程底线,提前拦截截断重叠大图大字
harmonyos
威哥爱编程1 天前
HarmonyOS 7 安全相机 2.0 实战:数字内容溯源,为拍摄内容提供来源保护
harmonyos