概述
在Leaflet中,DivOverlay
是Popup
和Tooltip
的基类. DivOverlay
提供了创建可自定义内容的弹出框和提示框的基础功能.
源码分析
源码实现
DivOverlay
的源码实现如下:
export var DivOverlay = Layer.extend({
options: {
interactive: false, // 是否响应鼠标事件(点击、悬停)
offset: [0, 0], // 覆盖层相对于锚点的像素偏移
pane: undefined, // 指定地图的窗格以控制叠放层级
content: "", // 覆盖层内容,支持动态函数生成
},
initialize: function (options, source) {
if (options && (options instanceof LatLng || Util.isArray(options))) {
this._latlng = toLatLng(options); // 转换坐标对象
Util.setOptions(this, source);
} else {
Util.setOptions(this, options);
this._source = source; // 关联的图层
}
if (this.options.content) {
this._content = this.options.content;
}
},
openOn: function (map) {
map = arguments.length ? map : this._source._map;
if (!map.hasLayer(this)) {
map.addLayer(this);
}
return this;
},
close: function () {
if (this._map) {
this._map.removeLayer(this);
}
return this;
},
toggle: function (layer) {
if (this._map) {
this.close();
} else {
if (argument.length) {
this._source = layer;
} else {
layer = this._source;
}
this._prepareOpen();
this.openOn(layer._map);
}
},
onAdd: function (map) {
this._zoomAnimated = map._zoomAnimated;
if (!this._container) {
this._initLayout();
}
if (map._fadeAnimated) {
DomUtil.setOpacity(this._container, 0);
}
clearTimeOut(this._removeTimeOut);
this.getPane().appendChild(this._container);
this.update();
if (map._fadeAnimated) {
DomUtil.setOpacity(this._container, 1);
}
this.bringToFront();
if (this.options.interactive) {
DomUtil.addClass(this._container, "leaflet-interactive");
this.addInteractiveTarget(this._container);
}
},
onRemove: function (map) {
if (map._fadeAnimated) {
DomUtil.setOpacity(this._container, 0);
this._removeTimeout = setTimeout(
Util.bind(DomUtil.remove, undefined, this._container),
200
);
} else {
DomUtil.remove(this._container);
}
if (this.options.interactive) {
DomUtil.removeClass(this._container, "leaflet-interactive");
this.removeInteractiveTarget(this._container);
}
},
getLatLng: function () {
return this._latlng;
},
setLatLng: function (latlng) {
this._latlng = toLatLng(latlng);
if (this._map) {
this._updatePosition();
this._adjustPan();
}
return this;
},
getContent: function () {
return this._content;
},
setContent: function (content) {
this._content = content;
this.update();
return this;
},
getElement: function () {
return this._container;
},
update: function () {
if (!this._map) {
return;
}
this._container.style.visibility = "hidden";
this._updateContent(); // 更新内容
this._updateLayout(); // 调整样式
this._updatePosition(); // 重新定位
this._container.style.visibility = "";
this._adjustPan();
},
getEvents: function () {
var events = {
zoom: this._updatePosition,
viewreset: this._updatePosition,
};
if (this._zoomAnimated) {
events.zoomanim = this._animateZoom;
}
return events;
},
isOpen: function () {
return !!this._map && this._map.hasLayer(this);
},
bringToFront: function () {
if (this._map) {
DomUtil.toFront(this._container);
}
return this;
},
bringToBack: function () {
if (this._map) {
DomUtil.toBack(this._container);
}
return this;
},
_prepareOpen: function (latlng) {
var source = this._source;
if (!source._map) {
return false;
}
if (source instanceof FeatureGroup) {
source = null;
var layers = this._source._layers;
for (var id in layers) {
if (layers[id]._map) {
source = layers[id];
break;
}
}
if (!source) {
return false;
}
this._source = source;
}
if (!latlng) {
if (source.getCenter) {
latlng = source.getCenter();
} else if (source.getLatLng) {
latlng = source.getLatLng();
} else if (source.getBounds) {
latlng = source.getBounds().getCenter();
} else {
throw new Error("Unable to get source layer LatLng.");
}
}
this.setLatLng(latlng);
if (this._map) {
this.update();
}
return true;
},
_updateContent: function () {
if (!this._content) {
return;
}
var node = this._contentNode;
var content =
typeof this._content === "function"
? this._content(this._source || this)
: this._content;
if (typeof content === "string") {
node.innerHTML = content;
} else {
while (node.hasChildNodes()) {
node.removeChild(node.firstChild);
}
node.appendChild(content);
}
this.fire("contentupdate");
},
_updatePosition: function () {
if (!this._map) {
return;
}
var pos = this._map.latLngToLayerPoint(this._latlng),
offset = toPoint(this.options.offset),
anchor = this._getAnchor();
if (this._zoomAnimated) {
DomUtil.setPosition(this._container, pos.add(anchor));
} else {
offset = offset.add(pos).add(anchor);
}
var bottom = (this._containerBottom = -offset.y),
left = (this._containerLeft =
-Math.round(this._containerWidth / 2) + offset.x);
this._container.style.bottom = bottom + "px";
this._container.style.left = left + "px";
},
_getAnchor: function () {
return [0, 0];
},
});
Map.include({
_initOverlay: function (OverlayClass, content, latlng, options) {
var overlay = content;
if (!(overlay instanceof OverlayClass)) {
overlay = new OverlayClass(options).setContent(content);
}
if (latlng) {
overlay.setLatLng(latlng);
}
return overlay;
},
});
Layer.include({
_initOverlay: function (OverlayClass, old, content, options) {
var overlay = content;
if (overlay instanceof OverlayClass) {
Util.setOptions(overlay, options);
overlay._source = this;
} else {
overlay = old && !options ? old : new OverlayClass(options, this);
overlay.setContent(content);
}
return overlay;
},
});
源码详解
继承与功能定位
- 继承自
Layer
:具备图层生命周期管理能力(添加/移除,事件监听) - 核心功能:
- 管理DOM容器的位置、内容、样式
- 处理与地图的交互(如位置随地图移动、缩放)
- 提供统一的API给子类(如
openOn
、setContent
)
核心方法解析
1.初始化与参数处理initialize
- 灵活参数:允许直接传入坐标或通过
source
图层派生位置
2.生命周期方法
-
onAdd(map)
- 初始化DOM容器(
_initLayout
) - 处理淡入动画(
map._fadeAnimated
) - 绑定交互事件(若
interactive:true
)
- 初始化DOM容器(
-
onRemove(map)
- 处理淡出动画或直接移除DOM
- 解绑交互事件
3.内容与位置更新
-
setContent(content)
- 更新内容并调用
update()
重绘
- 更新内容并调用
-
update()
- 三步更新:内容 → 布局 → 位置,避免中间状态闪烁
4.位置计算
- 动态定位:随地图缩放、平移实时更新位置
5.事件处理
- 监听地图事件:确保覆盖层位置与地图状态同步
关键内部方法
1._prepareOpen
- 智能定位:自动从关联图层如
Marker
获取位置,或使用显示坐标
2._updateContent
- 内容灵活性:支持静态内容或基于图层状态的动态内容
3._updatePosition
- 精确定位:计算并应用偏移量,确保覆盖层与地图元素对齐
4._getAnchor
- 自定义锚点:子类可重写以调整覆盖层的锚点位置
扩展方法
通过Map.include
和Layer.include
分别扩展Map
和Layer
的积累方法_initOverlay
,简化了覆盖层的创建和管理。
设计亮点
1.分层架构:
将 DOM 操作、位置计算、事件处理抽离到基类,子类只需扩展差异(如 Popup
定义箭头样式)。
2.性能优化:
-
更新时隐藏容器避免闪烁。
-
使用 CSS 变换(zoomAnimated)实现平滑缩放。
3.灵活性: -
内容支持函数动态生成。
-
自动从关联图层派生位置
应用场景
- 自定义信息框:如
Popup
、Tooltip
,用于显示额外信息 - 交互元素:如标记的自定义图标、信息框
- 自定义控件:如
ScaleControl
、ZoomControl
,用于扩展地图功能
总结
DivOverlay
是 Leaflet 浮动覆盖层的核心抽象,其实现围绕 DOM 管理、位置同步、内容更新展开,通过高度复用的代码为 Popup
和 Tooltip
提供基础能力。可通过扩展此类实现定制化覆盖层,兼顾功能与性能。