Deacon的小站

今天的天气也挺不错

uniapp从0 - 1实现微信小程序地图选择位置(支持卫星图)

前言

小程序地图组件为腾讯地图,功能上使用了高德服务端API,文中经纬度格式都是gcj02,高德API返回和接收的格式也是这个,高德API中需要的AMAP_WEBAPI_KEY, 请自行前往高德开发者中心注册获取。

真机演示视频

www.aliyundrive.com/s/StwocUooK… 手机端观看直接复制链接到浏览器,视频为手机录屏请退出全屏观看(尺寸不对)。

实现页面地图样式

初始化地图

首先我们需要一个全屏的地图,设置当前页面为沉浸式页面(取消导航栏 pages.json style.navigationStyle: custom),然后进入页面创建地图组件。

注意点:地图一定要设置固定宽度和高度

image.png

  <view class="choose-loacation-box">
    <view class="map-box">
      <map
        @regionchange="regionChange"
        :latitude="latitude"
        :longitude="longitude"
        :markers="markers"
        :show-location="true"
        :enable-satellite="true"
        id="_baseMapDom"
        ref="_baseMapDom"
        style="width: 750rpx; height: 100%"
      />
    </view>
  </view>
  
  // 设置样式
  .choose-loacation-box{
      position: relative;
      flex: 1;
      display: flex;
      flex-direction: column;
      height: 100%;
      width: 100%;
      .map-box{
        width: 100%;
        flex-shrink: 0;
      }
  }

给地图添加定位,中心指针

地图功能需要的有,获取当前位置,中心指针指示当前位置。中心指针很容易实现,因为我们的地图是全屏的设置,所以设置指针垂直水平居中即可。

注意点1:我们的地图底部会有信息展示,(机型高度)如果地图的一半高度低于信息栏的高度,那么就会出现底部信息栏遮挡地图中心指针的问题,所以我们需要给地图外层的元素增加高度。避免出现这种情况

注意点2:指针居中的问题,想象一下我们有一条40px高的直线,直线在屏幕上居中。它现在的正中心点其实是,这个直线的中心,也就是20px高度处。但是实际上我们地图上的指示指针是直线的最底部的那一个点需要居中,这样我们才能看见这个底部直线的点在移动过程中指向的位置到底是地图上的哪个位置。

注意点3:为什么我们需要这么注意指针点居中的问题,因为地图图层和指针图层是分隔的,我们不知道当前移动了地图后的位置是什么地方。所幸地图有个API,获取当前地图中心点位置。所以我们要在视图上,也就是用户所看见的视觉效果中,保证指针尾部的点与地图中心点重合。

注意点4: 如何验证地图中心点和指针中心点是否重合呢? 因为我们指针是固定的位置,一直在中心点不动,所以只需要获取地图中心点的位置后,设置一个maker在地图中心点,查看它们两个是否重合,如果重合了那么说明中心点位置设置正确,如果没有就需要检查你的指针中心点位置。

image.png

<!--地图中心指针-->
<view class="map-point-location">
  <view class="point-content-box">
    <view class="circle"></view>
    <view class="column"></view>
  </view>
</view>

<!-- 当前定位按钮 -->
<view class="current-location" :style="{top: (WXBUTTONINFO.top + 80) + 'px'}" @click="getLocation">
  <image src="/static/images/public/location.png" class="icon"></image>
</view>

// 设置样式

// 修改之前的地图盒子样式
  .map-box{
    height: calc(100% + 200rpx);
    width: 100%;
    flex-shrink: 0;
    transform: translateY(-200rpx); // 因为高度增加了,所以要偏移高度这样才能正确的设置中心点(地图偏移), 防止底部信息遮挡中心点坐标
  }
  
// 中心指针样式
  .map-point-location{
    position: fixed;
    left: calc(50% - 20rpx);
    top: calc(50% - 40rpx);
    display: flex;
    justify-content: center;
    align-items: center;
    transform: translateY(-200rpx);// 同地图偏移
    .point-content-box{
      display: flex;
      flex-direction: column;
      height: 80rpx;
      width: 40rpx;
      align-items: center;
      justify-content: center;
      margin-top: 60rpx; // circle 高度 40 * 50% = 20; + column 40 = 60rpx; 保证指针的尾部正确在视窗的最中心点
      .circle{ // 指针头部的圆
        height: 40rpx;
        width: 40rpx;
        background-color: #fff;
        border-radius: 50%;
        border-width: 16rpx;
        border-color: #71CB21;
        border-style: solid;
      }
      .column{ // 真正的指针
        width: 6rpx;
        height: 40rpx;
        background-color: #71CB21;
        border-bottom-left-radius: 8rpx;
        border-bottom-right-radius: 8rpx;
      }
    }
  }

添加地图底部面板信息和返回上级页面按钮

当前页面是沉浸式页面,没有导航栏所以需要自己设置一个,底部面板信息用来展示当前位置和提供搜索位置的功能。

注意点:提供搜索框和选中某搜索项目的active样式。

image.png

<!-- 返回按钮  WXBUTTONINFO 是获取的微信胶囊按钮信息 -->
<view class="head-item" :style="{top: WXBUTTONINFO.top + 'px'}">
  <view class="cancel" @click="onBack">返回</view>
</view>

<!-- 底部面板信息 -->
<view class="content-box">
  <view class="head">
    <view class="search">
      <input class="search-input" @input="keywordsChange" :value="keywords" confirm-type="search" placeholder="搜索地点" />
      <image class="empty" v-if="keywords" @click="keywords = ``" src="/static/images/public/fail_icon.png"></image>
    </view>
    <view class="confirm" @click="confirmChoose">完成</view>
  </view>
  <view class="row-box">
    <view class="row" v-for="v in addressList" :key="v.id">
      <view class="address" @click="selectItem(v)">
        <view class="address-name">{{ v.name }}</view>
        <view class="address-desc">{{ v.district }}{{ isAddress(v.address)}}</view>
      </view>
      <view class="check-icon">
        <u-icon v-if="v.check" class="check-state" name="checkbox-mark" color="#71CB21" size="24"></u-icon>
      </view>
    </view>
  </view>
</view>

// 设置样式
  .head-item{
    width: 100%;
    position: fixed;
    display: flex;
    justify-content: space-between;
    padding: 0 24rpx;
    .cancel{
      padding: 16rpx 24rpx;
      border-radius: 16rpx;
      background: #fff;
      color: #333;
      font-size: 32rpx;
    }
  }
  .content-box{
    flex: 1;
    // padding: 24rpx;
    position: fixed;
    bottom: 0;
    left: 12rpx;
    background: rgba(255, 255, 255, 0.92);
    width: calc(100% - 24rpx);
    box-shadow: 0px 0px 18rpx 0px rgba(187, 187, 187, 0.32);
    border-radius: 16rpx 16rpx 0px 0px;
    padding-bottom: env(safe-area-inset-bottom);
    .head{
      padding: 24rpx;
      background: linear-gradient(90deg, #E6FED7, rgba(247,251,210,0.2));
      border-radius: 16rpx 16rpx 0rpx 0rpx;
      display: flex;
      .search{
        width: 100%;
        height: 66rpx;
        border-radius: 16rpx;
        background: #ddd;
        padding: 8rpx 16rpx;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 28rpx;
        .search-input{
          height: 50rpx;
          // border: 1px solid #fff;
          color: #333;
          width: calc(100% - 60rpx);
          padding-left: 10rpx;
          padding-right: 30rpx;
        }
        .empty{
          height: 32rpx;
          width: 32rpx;
        }
      }
      .confirm{
        width: 120rpx;
        height: 66rpx;
        border-radius: 16rpx;
        display: flex;
        justify-content: center;
        align-items: center;
        background: #71CB21;
        color: #fff;
        font-size: 32rpx;
        margin-left: 24rpx;
      }
    }
    .row-box{
      height: 500rpx;
      background: #fff;
      overflow-x: hidden;
      padding: 32rpx 24rpx;
      .row{
        display: flex;
        padding-bottom: 12rpx;
        margin-top: 20rpx;
        border-bottom: 1rpx solid #999999;
        &:first-child{
          margin-top: 0;
        }
        &:last-child{
          border-bottom: none;
        }
        .address{
          flex: 1;
          .address-name{
            line-height: 32rpx;
            font-size: 28rpx;
            font-weight: 500;
            color: #333333;
          }
          .address-desc{
            line-height: 28rpx;
            font-size: 24rpx;
            font-weight: 500;
            color: #999999;
            margin-top: 12rpx;
          }
        }
        .check-icon{
          width: 50rpx;
          margin-left: 16rpx;
          flex-shrink: 0;
        }
      }
    }

  }

实现地图功能

前面我们把整体的地图样式已经设置完毕,现在就要给它加上应有的功能点。

初始化地图和基础配置

image.png

  data() { // 初始化基础数据
    return {
      latitude: 30.651192,
      longitude: 104.041016,
      markers: [],
      currentMapInstance: {}, // map 实例
      props: {}, // 额外配置,因为们这个是独立页面。所以props只能通过 options 来设置

      isMove: false, // 调用移动地图位置时设为true 避免多次触发 regionChange 函数
      keywords: "",
      addressList: []
    };
  },

init函数

创建地图实例和获取当前位置

init() {
  this.currentMapInstance = uni.createMapContext("_baseMapDom", this);
  this.getLocation();
},

getLocation 函数

当前函数是通用函数,我们页面中只要有需要获取当前位置的功能就可直接调用它。

获取位置之后,我们需要把位置信息转换为文字信息,并移动地图中心点位置到获取的位置。

getLocation() {
  uni.getLocation({
    type: "gcj02",
    geocode: true,
    success: (res) => {
      // 小程序中无需设置当前位置icon,自动就有位置标注
      // this.markers = [{ id: 1, title: "我的位置", latitude: res.latitude, longitude: res.longitude }];
      this.setCurrentCenter(res);
      this.setCurrentAddress(res);
      console.log("getLocation res", res);
    },
    fail: err => {
      if (err?.errMsg.indexOf("频繁调用会增加电量损耗") !== -1) {
        this.toast(err?.errMsg || "定位失败, 请检查是否开启GPS定位");
      }
    }
  })
},

setCurrentAddress 函数

当前函数是经纬度转换地理位置,使用了高德API。

获取位置后并将当前位置设置到地图面板信息中,供用户选择。

setCurrentAddress(centerLocation) {
  const location = [centerLocation.longitude, centerLocation.latitude].join(",");
  amapGeocode( // 经纬度转地址
    { location: location },
    success => {
      const currentAddress = success?.data?.regeocode?.formatted_address;
      this.addressList = [
        {
          name: currentAddress,
          id: "currentLocationAddress",
          location,
          district: [centerLocation.longitude.toFixed(7), centerLocation.latitude.toFixed(7)].toString(),
          address: "",
          check: true
        }
      ]
    },
    error => {
      console.log("amapGeocode error", error);
      this.toast("经纬度转地址失败");
    }
  );
},

// server.js
import { AMAP_WEBAPI_KEY } from "@/common/const";

// 高德地图逆地址编码 https://lbs.amap.com/api/webservice/guide/api/georegeo#introduce
const amapGeocode = (data, success, fail) => {
  uni.request({
    url: "https://restapi.amap.com/v3/geocode/regeo",
    method: "GET",
    data: { key: AMAP_WEBAPI_KEY, ...data },
    success(res) {
      success(res)
    },
    fail(err) {
      fail(err)
    }
  })
}

setCurrentCenter 设置地图中心点位置

注意点: 地图在执行API的过程中,也会重复触发我们在地图组件中绑定的事件 所以用一个全局变量isMove来判断是用户移动,还是我们手动移动。

setCurrentCenter(lnglat) {
  this.isMove = true;
  this.currentMapInstance.moveToLocation({
    longitude: Number(lnglat.longitude),
    latitude: Number(lnglat.latitude),
    success: res => {
      setTimeout(() => { // 地图移动有动画时间,用延时避免移动过程中触发 regionChange 函数
        this.isMove = false
      }, 1200)
    }
  });
},

regionChange 监听地图视野变化

当前函数用于监听用户拖动了地图后触发,用于展示当前位置的地理信息。

// 缩放地图时不会触发当前函数 实际真机缩放时会触发(但是移动的距离很小,基本上不可见)不知道是否是缩放的过程中手指略微移动了地图
regionChange({ detail } = {}) {
  if (this.isMove) return; // 手动设置地图中心点位置时会触发当前函数, 添加拦截判断.
  const { centerLocation } = detail;
  if (centerLocation) this.setCurrentAddress(centerLocation);
  console.log("centerLocation", centerLocation);
},

keywordsChange 监听用户输入

监听用户输入的文字,并根据内容进行地址查询,使用了高德API,最后将返回的数据展示在底部面板中。

image.png

keywordsChange(input) {
  const text = input?.target?.value;
  this.keywords = text;
  if (text) {
    amapAassistant(
      { keywords: text },
      success => {
        const { data: { status, tips } = {} } = success;
        if (status === "1" && Array.isArray(tips)) {
          // 过滤搜索出来的地址必须有location且为字符串类型
          this.addressList = tips.filter(tip => typeof tip.location === "string");
        } else this.addressList = [];
      },
      err => {
        this.toast(err.message || JSON.stringify(err));
      }
    );
  }
},

// server.js

// 高德输入提示
const amapAassistant = (data, success, fail) => {
  uni.request({
    url: "https://restapi.amap.com/v3/assistant/inputtips",
    method: "GET",
    data: { key: AMAP_WEBAPI_KEY, ...data },
    success(res) {
      success(res);
    },
    fail(err) {
      fail(err);
    }
  });
};

selectItem 点击搜索项

底部面板出现搜索列表时,点击其中某项设置为选中状态并移动地图中心点到选中的经纬度。

image.png

selectItem({ id }) {
  this.addressList = this.addressList.map(v => {
    return { ...v, check: id === v.id };
  });
  const checkItem = this.addressList.find(v => v.check);
  if (checkItem) {
    const location = checkItem.location?.split(",");
    const lnglat = { longitude: location[0], latitude: location[1] };
    this.setCurrentCenter(lnglat);
  }
},

confirmChoose 确认选择

当前函数是点击完成按钮后触发,将选择的地址提交给上一个页面使用。

confirmChoose() {
  const checkItem = this.addressList.find(v => v.check);
  if (!checkItem) {
    this.toast("请选择位置");
    return;
  }
  const eventChannel = this.getOpenerEventChannel(); // 官方API 直接调用即可
  uni.navigateBack({
    delta: 1,
    success: () => {
      eventChannel.emit("chooseLocation", checkItem);
    }
  })
},

// pre page (需要获取地址的页面,使用这个逻辑跳转选择地址页面,选择页面点击了完成按钮就会触发回调)
uni.navigateTo({
  url: "/pages/chooseLocation/index",
  events: {
    chooseLocation: (data) => {
      if (data?.name) {
        const location = data.location.split(",");
        data.longitude = Number(location[0]);
        data.latitude = Number(location[1]);
        const name = data.address ? `${data.name}-${data.address}` : data.name; // 如果本身存在地址 name-address 否则 name
        data.address = data.id === "currentLocationAddress" ? data.name : name; // 重新拼写地址信息
        this.address = data;
      }
      console.log("chooseLocation", data);
    }
  }
})

一些功能函数

简化页面逻辑的一些可复用函数

onBack() {
  uni.navigateBack();
},

isAddress(address) {
  if (typeof address === "string") return address ? `-${address}` : "";
  if (Array.isArray(address)) return address.length ? `-${address.toString()}` : "";
},

toast(title, time) {
  uni.showToast({
    title: title,
    duration: time || 3000,
    icon: "none"
  });
}

结语

以上就是完整实现一个地图选择位置页面的设计思路和代码实现,如果需要扩展配置项,那么你可以通过options去合并到props上,然后在页面中使用 props.xx 的属性去配置信息。

最后看到这里的朋友们请点个赞吧!