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

相关推荐
24白菜头2 小时前
【无标题】
c++·笔记·学习·harmonyos
LeesonWong3 小时前
Neo 构建鸿蒙应用【二】:技术路线全解
harmonyos
LeesonWong3 小时前
Neo 构建鸿蒙应用【三】:实战社交应用与工程感悟
harmonyos
xmdy58664 小时前
Flutter+开源鸿蒙实战|智联邻里Day6 引入GetX全局架构+升级版下拉刷新+Toast弹窗+网络状态监听
flutter·开源·harmonyos
斯班奇的好朋友阿法法4 小时前
鸿蒙 vs iOS vs 微信小程序:开发平台全面对比
ios·微信小程序·harmonyos
xmdy58665 小时前
Flutter+开源鸿蒙实战|智联邻里Day5 闲置详情页+删除功能+下拉刷新+交互优化
flutter·开源·harmonyos
maaath5 小时前
【maaath】Flutter for OpenHarmony 媒体工具应用开发实战
flutter·华为·harmonyos
nashane5 小时前
HarmonyOS 6学习:应用推广引擎评论管理与长截图自动拼接实战
学习·华为·harmonyos·harmonyos 5
key_3_feng6 小时前
鸿蒙基于润和DAYU200(RK3568)开发板的系统移植与实战开发
华为·harmonyos
Swift社区6 小时前
Store + System:鸿蒙游戏黄金分层
游戏·华为·harmonyos