HarmonyOS中开发高德地图第六篇:POI搜索功能

57 阅读6分钟

第六篇:POI搜索功能

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

学习目标

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

1. POI搜索类型

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

2. 核心类说明

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

3. 完整代码示例

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

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 常用方法

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 常用设置

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)

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

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会返回推荐城市:

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 搜索特定类型

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

本篇小结

本篇教程我们学习了:

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

下一篇我们将学习地理编码与逆地理编码。 班级 developer.huawei.com/consumer/cn…

源码地址 gitcode.com/daleishen/g…