[鸿蒙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

相关推荐
盐焗西兰花9 小时前
鸿蒙学习实战之路 - 应用追踪实践最佳实践
学习·华为·harmonyos
大雷神10 小时前
[鸿蒙2025领航者闯关]HarmonyOS中开发高德地图第二篇:显示第一个地图
harmonyos
ujainu10 小时前
Flutter与DevEco Studio协同开发:HarmonyOS应用实战指南
flutter·华为·harmonyos
赵财猫._.11 小时前
【Flutter x 鸿蒙】第四篇:双向通信——Flutter调用鸿蒙原生能力
flutter·华为·harmonyos
赵财猫._.12 小时前
【Flutter x 鸿蒙】第五篇:导航、路由与多设备适配
flutter·华为·harmonyos
Chaunceyin13 小时前
浅谈Openharmony 和 HarmonyOS
华为·harmonyos
●VON15 小时前
从单端到“空地一体”:基于 HarmonyOS 的多端协同感知系统开发实践
学习·华为·harmonyos·openharmony·开源鸿蒙
2401_8603195215 小时前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Overlay 遮罩层(创建一个遮罩层)
react native·react.js·harmonyos
2401_8604947016 小时前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Slider 滑块(用于在给定的范围内选择一个值)
react native·react.js·harmonyos